From 92ec2a34ac35f5ffa7b6933a03c0fc691d4639bc Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Ducasse Date: Wed, 9 Sep 2026 16:36:52 +0200 Subject: [PATCH] ingest server --- .github/workflows/deploy-ingest.yml | 24 + .github/workflows/docker-publish.yml | 10 +- .github/workflows/test.yml | 4 + AGENTS.md | 1 + DOCKER.md | 11 + .../app/(dashboard)/incidents/[id]/page.tsx | 157 + .../incidents/[id]/promote-dialog.tsx | 118 + .../app/(dashboard)/incidents/breadcrumb.tsx | 13 + .../src/app/(dashboard)/incidents/layout.tsx | 28 + .../src/app/(dashboard)/incidents/page.tsx | 61 + .../monitors/[id]/incidents/layout.tsx | 2 +- .../monitors/[id]/incidents/page.tsx | 2 +- .../src/app/(dashboard)/overview/layout.tsx | 2 +- .../src/app/(dashboard)/overview/page.tsx | 4 +- .../integrations/alert-sources-card.tsx | 140 + .../integrations/dead-letters-card.tsx | 82 + .../settings/integrations/page.tsx | 4 + .../chat/tool-renderers/get-incident.tsx | 31 + .../components/chat/tool-renderers/index.tsx | 45 + .../chat/tool-renderers/list-incidents.tsx | 40 + .../data-table/incidents/columns.tsx | 2 +- .../incidents/data-table-row-actions.tsx | 6 +- .../data-table/incidents/dialog-confirm.tsx | 10 +- .../overview-events/incident-action-cell.tsx | 2 +- .../workspace-incidents/columns.tsx | 91 + .../src/components/nav/app-sidebar.tsx | 5 + apps/dashboard/src/config/nav.ts | 8 + .../src/data/overview-events.client.ts | 2 +- apps/ingest/.dockerignore | 13 + apps/ingest/.gitignore | 2 + apps/ingest/AGENTS.md | 79 + apps/ingest/Dockerfile | 90 + apps/ingest/deno.jsonc | 6 + apps/ingest/dofigen.lock | 242 + apps/ingest/dofigen.yml | 100 + apps/ingest/fly.toml | 45 + apps/ingest/package.json | 38 + apps/ingest/src/__tests__/accept.test.ts | 158 + apps/ingest/src/__tests__/drain.test.ts | 242 + apps/ingest/src/__tests__/helpers.ts | 161 + apps/ingest/src/__tests__/limits.test.ts | 111 + apps/ingest/src/env.ts | 18 + apps/ingest/src/errors.ts | 55 + apps/ingest/src/inbox/accept.ts | 197 + apps/ingest/src/inbox/drain.ts | 308 + apps/ingest/src/inbox/retention.ts | 15 + apps/ingest/src/inbox/scheduler.ts | 78 + apps/ingest/src/inbox/sweep.ts | 65 + apps/ingest/src/index.ts | 39 + apps/ingest/src/lib/auth.ts | 92 + apps/ingest/src/lib/dedup.ts | 46 + apps/ingest/src/lib/rate-limit.ts | 37 + apps/ingest/src/routes/cron.ts | 11 + apps/ingest/src/routes/health.ts | 5 + apps/ingest/src/routes/ingest.ts | 36 + apps/ingest/src/serve.ts | 46 + apps/ingest/tsconfig.json | 14 + apps/screenshot-service/src/index.ts | 14 +- apps/server/src/routes/mcp/handler.test.ts | 6 + apps/server/src/routes/mcp/server.ts | 2 + apps/server/src/routes/mcp/tools/incident.ts | 29 + apps/server/src/routes/public/status.test.ts | 28 +- apps/server/src/routes/public/status.ts | 5 +- .../rpc/handlers/incident/converters.ts | 92 + .../routes/rpc/handlers/incident/errors.ts | 16 + .../src/routes/rpc/handlers/incident/index.ts | 195 + .../monitor/__tests__/monitor.test.ts | 13 +- apps/server/src/routes/rpc/router.ts | 3 + apps/server/src/routes/v1/incidents/get.ts | 8 +- .../src/routes/v1/incidents/get_all.test.ts | 12 +- .../server/src/routes/v1/incidents/get_all.ts | 6 +- apps/server/src/routes/v1/incidents/put.ts | 12 +- .../routes/v1/monitors/trigger/post.test.ts | 23 +- apps/server/static/openapi-yaml.ts | 2 +- apps/server/static/openapi.json | 956 ++- apps/server/static/openapi.yaml | 670 +- .../status-page/status-calendar.tsx | 2 +- apps/web/src/content/docs.config.ts | 4 + .../concept/status-reports-and-incidents.mdx | 36 +- .../guides/how-to-connect-alert-sources.mdx | 105 + apps/workflows/src/checker/alerting.test.ts | 13 + apps/workflows/src/checker/alerting.ts | 8 +- apps/workflows/src/checker/index.ts | 2 +- ...ent-utils.ts => monitor-incident-utils.ts} | 20 +- apps/workflows/src/checker/outbox.ts | 20 +- .../workflows/src/checker/private-location.ts | 4 +- apps/workflows/src/checker/transition.test.ts | 30 +- apps/workflows/src/checker/transition.ts | 34 +- .../src/checker/update-status.test.ts | 6 +- apps/workflows/src/cron/status-drift.test.ts | 8 +- apps/workflows/src/cron/status-drift.ts | 2 +- apps/workflows/src/incident/index.ts | 14 +- .../workflows/src/scripts/outbox-preflight.ts | 17 +- coolify-deployment.yaml | 34 + deno.lock | 16 + docker-compose.yaml | 34 + oxlint.config.ts | 3 +- packages/alert-adapters/package.json | 25 + packages/alert-adapters/src/index.test.ts | 17 + packages/alert-adapters/src/index.ts | 23 + .../src/providers/alertmanager/api-types.ts | 26 + .../src/providers/alertmanager/fixtures.ts | 74 + .../src/providers/alertmanager/index.ts | 25 + .../src/providers/alertmanager/mapper.test.ts | 72 + .../src/providers/alertmanager/mapper.ts | 54 + .../src/providers/grafana/api-types.ts | 34 + .../src/providers/grafana/fixtures.ts | 68 + .../src/providers/grafana/index.ts | 32 + .../src/providers/grafana/mapper.test.ts | 48 + .../src/providers/grafana/mapper.ts | 51 + packages/alert-adapters/src/types.ts | 28 + packages/alert-adapters/tsconfig.json | 8 + packages/analytics/src/events.ts | 12 + packages/api/src/edge.ts | 4 + packages/api/src/router/alertSource.ts | 122 + packages/api/src/router/incident.ts | 158 +- packages/api/src/router/monitorIncident.ts | 118 + .../api/src/router/statusPage.e2e.test.ts | 10 +- packages/api/src/router/statusPage.ts | 23 +- .../api/src/router/statusPage.utils.test.ts | 5 +- ...87_rename_incident_to_monitor_incident.sql | 7 + .../0088_incident_and_alert_source.sql | 45 + packages/db/drizzle/0089_alert_inbox.sql | 45 + packages/db/drizzle/meta/0087_snapshot.json | 6053 +++++++++++++++ packages/db/drizzle/meta/0088_snapshot.json | 6399 ++++++++++++++++ packages/db/drizzle/meta/0089_snapshot.json | 6721 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 21 + packages/db/package.json | 3 + .../db/src/schema/alert_inbox/alert_inbox.ts | 93 + .../db/src/schema/alert_inbox/constants.ts | 7 + packages/db/src/schema/alert_inbox/index.ts | 3 + .../db/src/schema/alert_inbox/validation.ts | 10 + .../src/schema/alert_sources/alert_source.ts | 49 + .../db/src/schema/alert_sources/constants.ts | 5 + packages/db/src/schema/alert_sources/index.ts | 3 + .../db/src/schema/alert_sources/validation.ts | 24 + .../db/src/schema/audit_logs/validation.ts | 17 +- packages/db/src/schema/incidents/constants.ts | 13 + packages/db/src/schema/incidents/incident.ts | 88 +- packages/db/src/schema/incidents/index.ts | 1 + .../db/src/schema/incidents/validation.ts | 9 +- packages/db/src/schema/index.ts | 3 + .../db/src/schema/monitor_incidents/index.ts | 2 + .../monitor_incidents/monitor_incident.ts | 110 + .../schema/monitor_incidents/validation.ts | 12 + packages/db/src/schema/monitors/monitor.ts | 4 +- .../db/src/schema/notifications/outbox.ts | 19 +- packages/db/src/schema/plan/config.ts | 4 + packages/db/src/schema/plan/schema.ts | 1 + packages/db/src/schema/shared.ts | 12 +- packages/db/src/seed.mts | 4 +- packages/notifications/base/src/types.ts | 4 +- .../base/src/utils/incident.test.ts | 6 +- .../notifications/base/src/utils/incident.ts | 4 +- .../notifications/base/src/utils/message.ts | 4 +- .../api/openstatus/incident/v1/incident.proto | 83 + .../api/openstatus/incident/v1/service.proto | 159 + packages/proto/gen/openapi.yaml | 670 +- .../ts/openstatus/incident/v1/incident_pb.ts | 249 + .../gen/ts/openstatus/incident/v1/index.ts | 3 + .../ts/openstatus/incident/v1/service_pb.ts | 483 ++ packages/proto/package.json | 4 + packages/services/package.json | 12 + packages/services/src/agent-tools/incident.ts | 200 + packages/services/src/agent-tools/index.ts | 22 + packages/services/src/alert-inbox/drain.ts | 194 + packages/services/src/alert-inbox/index.ts | 12 + packages/services/src/alert-inbox/prune.ts | 73 + packages/services/src/alert-inbox/record.ts | 50 + packages/services/src/alert-inbox/schemas.ts | 22 + .../__tests__/alert-source.test.ts | 163 + .../src/alert-source/find-or-create.ts | 97 + packages/services/src/alert-source/index.ts | 16 + .../services/src/alert-source/internal.ts | 22 + packages/services/src/alert-source/manage.ts | 117 + packages/services/src/alert-source/schemas.ts | 28 + .../__tests__/get-history.test.ts | 6 +- .../services/src/frozen-uptime/get-history.ts | 4 +- .../src/incident/__tests__/incident.test.ts | 410 +- packages/services/src/incident/acknowledge.ts | 42 +- .../services/src/incident/by-fingerprint.ts | 208 + packages/services/src/incident/create.ts | 46 + packages/services/src/incident/delete.ts | 11 +- packages/services/src/incident/index.ts | 26 +- packages/services/src/incident/internal.ts | 1 - .../src/incident/link-monitor-incident.ts | 56 + packages/services/src/incident/list.ts | 125 +- packages/services/src/incident/promote.ts | 75 + packages/services/src/incident/resolve.ts | 45 +- packages/services/src/incident/schemas.ts | 55 +- packages/services/src/incident/update.ts | 54 + packages/services/src/limits.ts | 10 + .../__tests__/monitor-incident.test.ts | 321 + .../src/monitor-incident/acknowledge.ts | 68 + .../services/src/monitor-incident/delete.ts | 36 + .../services/src/monitor-incident/index.ts | 20 + .../services/src/monitor-incident/internal.ts | 26 + .../services/src/monitor-incident/list.ts | 153 + .../services/src/monitor-incident/resolve.ts | 67 + .../services/src/monitor-incident/schemas.ts | 43 + packages/services/src/monitor/list.ts | 20 +- .../src/page-component/get-daily-summary.ts | 6 +- .../services/src/status-timeline/events.ts | 4 +- packages/services/src/types.ts | 9 +- packages/tracker/src/tracker.test.ts | 6 +- packages/tracker/src/tracker.ts | 4 +- packages/tracker/src/types.ts | 4 +- pnpm-lock.yaml | 96 +- 208 files changed, 30303 insertions(+), 1033 deletions(-) create mode 100644 .github/workflows/deploy-ingest.yml create mode 100644 apps/dashboard/src/app/(dashboard)/incidents/[id]/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/incidents/[id]/promote-dialog.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/incidents/breadcrumb.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/incidents/layout.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/incidents/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/settings/integrations/alert-sources-card.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/settings/integrations/dead-letters-card.tsx create mode 100644 apps/dashboard/src/components/chat/tool-renderers/get-incident.tsx create mode 100644 apps/dashboard/src/components/chat/tool-renderers/list-incidents.tsx create mode 100644 apps/dashboard/src/components/data-table/workspace-incidents/columns.tsx create mode 100644 apps/ingest/.dockerignore create mode 100644 apps/ingest/.gitignore create mode 100644 apps/ingest/AGENTS.md create mode 100644 apps/ingest/Dockerfile create mode 100644 apps/ingest/deno.jsonc create mode 100644 apps/ingest/dofigen.lock create mode 100644 apps/ingest/dofigen.yml create mode 100644 apps/ingest/fly.toml create mode 100644 apps/ingest/package.json create mode 100644 apps/ingest/src/__tests__/accept.test.ts create mode 100644 apps/ingest/src/__tests__/drain.test.ts create mode 100644 apps/ingest/src/__tests__/helpers.ts create mode 100644 apps/ingest/src/__tests__/limits.test.ts create mode 100644 apps/ingest/src/env.ts create mode 100644 apps/ingest/src/errors.ts create mode 100644 apps/ingest/src/inbox/accept.ts create mode 100644 apps/ingest/src/inbox/drain.ts create mode 100644 apps/ingest/src/inbox/retention.ts create mode 100644 apps/ingest/src/inbox/scheduler.ts create mode 100644 apps/ingest/src/inbox/sweep.ts create mode 100644 apps/ingest/src/index.ts create mode 100644 apps/ingest/src/lib/auth.ts create mode 100644 apps/ingest/src/lib/dedup.ts create mode 100644 apps/ingest/src/lib/rate-limit.ts create mode 100644 apps/ingest/src/routes/cron.ts create mode 100644 apps/ingest/src/routes/health.ts create mode 100644 apps/ingest/src/routes/ingest.ts create mode 100644 apps/ingest/src/serve.ts create mode 100644 apps/ingest/tsconfig.json create mode 100644 apps/server/src/routes/mcp/tools/incident.ts create mode 100644 apps/server/src/routes/rpc/handlers/incident/converters.ts create mode 100644 apps/server/src/routes/rpc/handlers/incident/errors.ts create mode 100644 apps/server/src/routes/rpc/handlers/incident/index.ts create mode 100644 apps/web/src/content/pages/docs/guides/how-to-connect-alert-sources.mdx rename apps/workflows/src/checker/{incident-utils.ts => monitor-incident-utils.ts} (80%) create mode 100644 packages/alert-adapters/package.json create mode 100644 packages/alert-adapters/src/index.test.ts create mode 100644 packages/alert-adapters/src/index.ts create mode 100644 packages/alert-adapters/src/providers/alertmanager/api-types.ts create mode 100644 packages/alert-adapters/src/providers/alertmanager/fixtures.ts create mode 100644 packages/alert-adapters/src/providers/alertmanager/index.ts create mode 100644 packages/alert-adapters/src/providers/alertmanager/mapper.test.ts create mode 100644 packages/alert-adapters/src/providers/alertmanager/mapper.ts create mode 100644 packages/alert-adapters/src/providers/grafana/api-types.ts create mode 100644 packages/alert-adapters/src/providers/grafana/fixtures.ts create mode 100644 packages/alert-adapters/src/providers/grafana/index.ts create mode 100644 packages/alert-adapters/src/providers/grafana/mapper.test.ts create mode 100644 packages/alert-adapters/src/providers/grafana/mapper.ts create mode 100644 packages/alert-adapters/src/types.ts create mode 100644 packages/alert-adapters/tsconfig.json create mode 100644 packages/api/src/router/alertSource.ts create mode 100644 packages/api/src/router/monitorIncident.ts create mode 100644 packages/db/drizzle/0087_rename_incident_to_monitor_incident.sql create mode 100644 packages/db/drizzle/0088_incident_and_alert_source.sql create mode 100644 packages/db/drizzle/0089_alert_inbox.sql create mode 100644 packages/db/drizzle/meta/0087_snapshot.json create mode 100644 packages/db/drizzle/meta/0088_snapshot.json create mode 100644 packages/db/drizzle/meta/0089_snapshot.json create mode 100644 packages/db/src/schema/alert_inbox/alert_inbox.ts create mode 100644 packages/db/src/schema/alert_inbox/constants.ts create mode 100644 packages/db/src/schema/alert_inbox/index.ts create mode 100644 packages/db/src/schema/alert_inbox/validation.ts create mode 100644 packages/db/src/schema/alert_sources/alert_source.ts create mode 100644 packages/db/src/schema/alert_sources/constants.ts create mode 100644 packages/db/src/schema/alert_sources/index.ts create mode 100644 packages/db/src/schema/alert_sources/validation.ts create mode 100644 packages/db/src/schema/incidents/constants.ts create mode 100644 packages/db/src/schema/monitor_incidents/index.ts create mode 100644 packages/db/src/schema/monitor_incidents/monitor_incident.ts create mode 100644 packages/db/src/schema/monitor_incidents/validation.ts create mode 100644 packages/proto/api/openstatus/incident/v1/incident.proto create mode 100644 packages/proto/api/openstatus/incident/v1/service.proto create mode 100644 packages/proto/gen/ts/openstatus/incident/v1/incident_pb.ts create mode 100644 packages/proto/gen/ts/openstatus/incident/v1/index.ts create mode 100644 packages/proto/gen/ts/openstatus/incident/v1/service_pb.ts create mode 100644 packages/services/src/agent-tools/incident.ts create mode 100644 packages/services/src/alert-inbox/drain.ts create mode 100644 packages/services/src/alert-inbox/index.ts create mode 100644 packages/services/src/alert-inbox/prune.ts create mode 100644 packages/services/src/alert-inbox/record.ts create mode 100644 packages/services/src/alert-inbox/schemas.ts create mode 100644 packages/services/src/alert-source/__tests__/alert-source.test.ts create mode 100644 packages/services/src/alert-source/find-or-create.ts create mode 100644 packages/services/src/alert-source/index.ts create mode 100644 packages/services/src/alert-source/internal.ts create mode 100644 packages/services/src/alert-source/manage.ts create mode 100644 packages/services/src/alert-source/schemas.ts create mode 100644 packages/services/src/incident/by-fingerprint.ts create mode 100644 packages/services/src/incident/create.ts create mode 100644 packages/services/src/incident/link-monitor-incident.ts create mode 100644 packages/services/src/incident/promote.ts create mode 100644 packages/services/src/incident/update.ts create mode 100644 packages/services/src/monitor-incident/__tests__/monitor-incident.test.ts create mode 100644 packages/services/src/monitor-incident/acknowledge.ts create mode 100644 packages/services/src/monitor-incident/delete.ts create mode 100644 packages/services/src/monitor-incident/index.ts create mode 100644 packages/services/src/monitor-incident/internal.ts create mode 100644 packages/services/src/monitor-incident/list.ts create mode 100644 packages/services/src/monitor-incident/resolve.ts create mode 100644 packages/services/src/monitor-incident/schemas.ts diff --git a/.github/workflows/deploy-ingest.yml b/.github/workflows/deploy-ingest.yml new file mode 100644 index 00000000..1e5acd6a --- /dev/null +++ b/.github/workflows/deploy-ingest.yml @@ -0,0 +1,24 @@ +name: Fly Deploy Ingest +on: + push: + branches: + - main + paths: + - "apps/ingest/**" + - "packages/alert-adapters/**" + - "packages/services/**" + - "packages/db/**" + workflow_dispatch: +jobs: + deploy: + name: Deploy Ingest + runs-on: depot-ubuntu-24.04-4 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: + flyctl deploy --config apps/ingest/fly.toml + --dockerfile apps/ingest/Dockerfile --remote-only --wait-timeout=500 + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7e44e722..5c1d4aeb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,6 +6,7 @@ on: - main paths: - "apps/server/**" + - "apps/ingest/**" - "apps/dashboard/**" - "apps/workflows/**" - "apps/private-location/**" @@ -18,7 +19,7 @@ on: services: description: 'Services to build (comma-separated)' required: false - default: 'server,dashboard,workflows,private-location,status-page,checker,db-migrate' + default: 'server,ingest,dashboard,workflows,private-location,status-page,checker,db-migrate' type: string concurrency: @@ -53,9 +54,9 @@ jobs: # packages/** changes affect all services if echo "$CHANGED" | grep -q '^packages/'; then - SERVICES_LIST=("server" "dashboard" "workflows" "private-location" "status-page" "checker" "db-migrate") + SERVICES_LIST=("server" "ingest" "dashboard" "workflows" "private-location" "status-page" "checker" "db-migrate") else - for svc in server dashboard workflows private-location status-page checker; do + for svc in server ingest dashboard workflows private-location status-page checker; do if echo "$CHANGED" | grep -q "^apps/$svc/"; then SERVICES_LIST+=("$svc") fi @@ -86,6 +87,9 @@ jobs: - service: server context: . dockerfile: apps/server/Dockerfile + - service: ingest + context: . + dockerfile: apps/ingest/Dockerfile - service: dashboard context: . dockerfile: apps/dashboard/Dockerfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b46e29eb..7d762e1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,9 @@ jobs: - name: workflows filter: "--filter=@openstatus/workflows" database: true + - name: ingest + filter: "--filter=@openstatus/ingest" + database: true - name: subscriptions filter: "--filter=@openstatus/subscriptions" database: true @@ -43,6 +46,7 @@ jobs: --filter=!@openstatus/server --filter=!@openstatus/api --filter=!@openstatus/workflows + --filter=!@openstatus/ingest --filter=!@openstatus/subscriptions database: false services: diff --git a/AGENTS.md b/AGENTS.md index 229300d0..ccfefebb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,7 @@ boundaries with external SDKs or at registry-style dispatch. When you need one: - `packages/ui/AGENTS.md` — stock shadcn vs. the published blocks registry - `apps/dashboard/AGENTS.md` — Next.js runtimes, client boundary, UI verification - `apps/server/AGENTS.md` — Hono API, API-key scopes +- `apps/ingest/AGENTS.md` — third-party alert webhooks, the inbox pattern - `apps/status-page/AGENTS.md` — public surfaces and gated content - `apps/workflows/AGENTS.md` — Deno runtime constraints - `apps/checker/AGENTS.md` — Go probing tier diff --git a/DOCKER.md b/DOCKER.md index c29c46a4..46192f9a 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -55,6 +55,7 @@ docker builder prune | workflows | 3000 | Background jobs | | server | 3001 | API backend (tRPC) | | dashboard | 3002 | Admin interface | +| ingest | 3004 | Third-party alert webhooks | | status-page | 3003 | Public status pages | | private-location | 8081 | Monitoring agent | | libsql | 8080 | Database (HTTP) | @@ -62,6 +63,16 @@ docker builder prune | tinybird-local | 7181 | Analytics | +### Alert ingest and rate limiting + +The `ingest` service receives third-party alert webhooks at +`POST /v1/ingest/`. Per-source rate limiting uses Upstash Redis, which +is **not part of this compose file**: without `UPSTASH_REDIS_REST_URL` and +`UPSTASH_REDIS_REST_TOKEN`, ingest runs normally but applies **no rate limit**. +That is deliberate — a missing cache must not stop alerts from being accepted — +but it means a runaway sender can fill `alert_inbox`. Set those variables if you +expose ingest to the internet. + ## Architecture ``` diff --git a/apps/dashboard/src/app/(dashboard)/incidents/[id]/page.tsx b/apps/dashboard/src/app/(dashboard)/incidents/[id]/page.tsx new file mode 100644 index 00000000..49551756 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/incidents/[id]/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { Badge } from "@openstatus/ui/components/ui/badge"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { toast } from "sonner"; + +import { + EmptyStateContainer, + EmptyStateTitle, +} from "@/components/content/empty-state"; +import { + Section, + SectionDescription, + SectionGroup, + SectionHeader, + SectionHeaderRow, + SectionTitle, +} from "@/components/content/section"; +import { useTRPC } from "@/lib/trpc/client"; + +import { PromoteDialog } from "./promote-dialog"; + +const ORIGIN_LABEL = { + monitor: "openstatus monitor", + external: "Ingested alert", + manual: "Opened by hand", +} as const; + +export default function Page() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const params = useParams<{ id: string }>(); + const id = Number(params.id); + + const { data: incident } = useQuery(trpc.incident.get.queryOptions({ id })); + const { data: auditLogs } = useQuery( + trpc.auditLog.list.queryOptions({ + entityType: "incident", + entityId: String(id), + limit: 100, + offset: 0, + }), + ); + + const invalidate = () => { + queryClient.invalidateQueries({ queryKey: trpc.incident.list.queryKey() }); + queryClient.invalidateQueries({ + queryKey: trpc.incident.get.queryKey({ id }), + }); + queryClient.invalidateQueries({ queryKey: trpc.auditLog.list.queryKey() }); + }; + + const acknowledge = useMutation( + trpc.incident.acknowledge.mutationOptions({ + onSuccess: invalidate, + onError: (error) => toast.error(error.message), + }), + ); + const resolve = useMutation( + trpc.incident.resolve.mutationOptions({ + onSuccess: invalidate, + onError: (error) => toast.error(error.message), + }), + ); + + if (!incident) return null; + + return ( + +
+ + + {incident.title} + + {ORIGIN_LABEL[incident.origin]} · started{" "} + {incident.startedAt.toLocaleString()} + + +
+ {!incident.acknowledgedAt && !incident.resolvedAt ? ( + + ) : null} + {!incident.resolvedAt ? ( + + ) : null} + {!incident.statusReportId ? ( + + ) : null} +
+
+
+ {incident.status} + + {incident.severity} + + {incident.autoResolved ? ( + auto-resolved + ) : null} + {incident.statusReportId ? ( + + published as report #{incident.statusReportId} + + ) : null} +
+ {incident.summary ? ( +

{incident.summary}

+ ) : null} +
+ +
+ + History + + Incidents have no separate timeline — this is the audit log. + + + {!auditLogs || auditLogs.items.length === 0 ? ( + + No recorded changes yet + + ) : ( +
    + {auditLogs.items.map((entry) => ( +
  • + {entry.action} + + {new Date(entry.createdAt).toLocaleString()} + +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/incidents/[id]/promote-dialog.tsx b/apps/dashboard/src/app/(dashboard)/incidents/[id]/promote-dialog.tsx new file mode 100644 index 00000000..4fd3d14a --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/incidents/[id]/promote-dialog.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { Button } from "@openstatus/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@openstatus/ui/components/ui/dialog"; +import { Label } from "@openstatus/ui/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@openstatus/ui/components/ui/select"; +import { Textarea } from "@openstatus/ui/components/ui/textarea"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { useTRPC } from "@/lib/trpc/client"; + +export function PromoteDialog({ + incidentId, + defaultTitle, +}: { + incidentId: number; + defaultTitle: string; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [pageId, setPageId] = useState(""); + const [message, setMessage] = useState(""); + + const { data: pages } = useQuery(trpc.page.list.queryOptions()); + + const promote = useMutation( + trpc.incident.promote.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.incident.list.queryKey(), + }); + queryClient.invalidateQueries({ + queryKey: trpc.incident.get.queryKey({ id: incidentId }), + }); + toast.success("Published as a status report"); + setOpen(false); + }, + onError: (error) => toast.error(error.message), + }), + ); + + return ( + + + + + + + Publish “{defaultTitle}” + + Creates a status report on the page you choose, seeded from this + incident. This is the only step that makes it public. + + +
+
+ + +
+
+ +