feat(studio): /api/status-page endpoint backed by incident.io Widget API (#50931)

## Summary

* Adds `/api/status-page` (Next route + TanStack wrapper), backed by the
[incident.io](<http://incident.io>) Widget API, annotating each item
with `visible`, `show_banner`, and (for scheduled maintenances)
`banner_lead_days`.
* Deployment-mode visibility is driven by a new
`status_page:visibility_field_ids` custom-content key.
* Widget array parsing is fault-tolerant: a malformed item in one array
is dropped and logged rather than failing the whole response, so one bad
item can't hide a real ongoing incident.
* 429s from [incident.io](<http://incident.io>) are retried with
equal-jitter exponential backoff, respecting `Retry-After`, up to 2
retries.
* Nothing consumes this endpoint yet — it replaces no existing behavior
and changes nothing user-visible. Later PRs (this is PR 1 of a stack)
wire up consumers behind the `incidentIoStatusPage` ConfigCat flag.

Part of
[FE-4057](https://linear.app/supabase/issue/FE-4057/frontend-bannerbot-reconfigured)
— see Linear for full design context.

## Test plan

- [X] `pnpm --filter studio run typecheck`
- [X] `pnpm --filter studio run lint:ratchet`
- [X] `pnpm knip --workspace apps/studio`
- [X] `pnpm test:prettier`
- [X] `pnpm --filter studio exec vitest run status-page` — 44 tests
passing, including a regression test built from a real production
[incident.io](<http://incident.io>) payload that initially failed to
parse, and a compile-time type-safety regression test for the
array-parsing helper

Co-authored-by: Claude Code
[charis@supabase.io](<mailto:charis@supabase.io>)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a status page that displays ongoing incidents and maintenance,
with visibility and banner settings based on linked incident details.
* Status page data is available through a new API endpoint, with caching
for successful responses and degraded results.
* **Bug Fixes**
* Status page data can still display when some linked incident details
are unavailable; affected results are marked as degraded.
* Improved handling of invalid widget entries so they don’t prevent
valid items from being processed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Code <charis@supabase.io>
This commit is contained in:
CharisandClaude Code authored and GitHub committed 2026-09-28 11:36:29 -04:00
1 parent 1b5a806963
commit b2cf3693dd
16 files changed
+1294 -4

No files matched your search

+3 -2
View File
@@ -442,8 +442,9 @@ per-file items only when a subtree has special cases.
- [x] `routes/api/v1/**` — except `body.ts` (streaming rewrite)
- [x] `routes/api/v1/projects/$ref/functions/$slug/body.ts` — Web-streams rewrite. Returns a `Response` whose body is a `ReadableStream`; each artifact file is converted via `Readable.toWeb(createReadStream(...))` and pulled chunk-by-chunk into the multipart stream. Skips the `apiWrapper` since `getFunctionsArtifactStore` already asserts self-hosted mode (the pages-router `withAuth` was a no-op outside `IS_PLATFORM`).
- [x] `routes/api/mcp/index.ts` — uses `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js`. Takes a Web `Request`, returns a `Response` directly — no shim needed. Query parsing pulled from `request.url`'s search params; headers passed through unchanged.
- [x] `routes/api/incident-banner.ts`, `routes/api/incident-status.ts` — App
Router routes under `app/api/**` (already Web-native, direct re-export)
- [x] `routes/api/incident-banner.ts`, `routes/api/incident-status.ts`,
`routes/api/status-page.ts` — App Router routes under `app/api/**`
(already Web-native, direct re-export)
---
+65
View File
@@ -0,0 +1,65 @@
import { IS_PLATFORM } from 'common'
import { NextResponse } from 'next/server'
import { InternalServerError } from '@/lib/api/apiHelpers'
import { getStatusPage } from '@/lib/api/status-page'
const CACHE_CONTROL_SUCCESS = 'public, s-maxage=300, stale-while-revalidate=300'
const CACHE_CONTROL_DEGRADED = 'public, s-maxage=60, stale-while-revalidate=60'
const CACHE_CONTROL_ERROR = 'no-store'
export async function OPTIONS() {
if (!IS_PLATFORM) return new Response(null, { status: 404 })
return new Response(null, {
status: 204,
headers: { Allow: 'GET, HEAD, OPTIONS' },
})
}
export async function HEAD() {
if (!IS_PLATFORM) return new Response(null, { status: 404 })
return new Response(null, {
status: 200,
headers: { 'Cache-Control': CACHE_CONTROL_SUCCESS },
})
}
export async function GET() {
if (!IS_PLATFORM) return new Response(null, { status: 404 })
try {
const { data, isDegraded } = await getStatusPage()
return NextResponse.json(data, {
headers: { 'Cache-Control': isDegraded ? CACHE_CONTROL_DEGRADED : CACHE_CONTROL_SUCCESS },
})
} catch (error) {
let errorCode = 500
const headers = new Headers({ 'Cache-Control': CACHE_CONTROL_ERROR })
if (error instanceof InternalServerError) {
if (typeof error.details?.status === 'number') errorCode = error.details.status
if (errorCode === 420) errorCode = 429
if (errorCode === 429 && typeof error.details?.retryAfter === 'string') {
headers.set('Retry-After', error.details.retryAfter)
}
console.error(
'Failed to fetch incident.io status page: %s',
JSON.stringify(
{
message: error.message,
details: error.details,
},
null,
2
)
)
} else {
console.error('Unexpected error fetching incident.io status page:', error)
}
return NextResponse.json(
{ error: { message: 'Internal server error' } },
{ status: errorCode, headers }
)
}
}
@@ -39,4 +39,6 @@ export type CustomContentTypes = {
infraAwsNimbusLabel: string
sslCertificateUrl: string
statusPageVisibilityFieldIds: Array<string>
}
@@ -60,5 +60,7 @@
"infra:cloud_providers": ["AWS", "AWS_K8S", "FLY"],
"infra:aws_nimbus_label": "AWS Nimbus",
"ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt"
"ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt",
"status_page:visibility_field_ids": ["", "01M3D1YZJ1FV5495GP53VMZKCY"]
}
@@ -60,5 +60,7 @@
"infra:cloud_providers": ["AWS_NIMBUS"],
"infra:aws_nimbus_label": "AWS Nimbus",
"ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt"
"ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt",
"status_page:visibility_field_ids": ["01TESTOWNFIELDID000000000", "02TESTOTHERFIELDID00000000"]
}
@@ -105,6 +105,14 @@
"ssl:certificate_url": {
"type": "string",
"description": "The URL of the SSL certificate"
},
"status_page:visibility_field_ids": {
"type": "array",
"description": "Deployment-mode visibility field IDs on incident.io response incidents. Element 0 is this build's own custom-field ID (empty string = default Supabase build); the rest are other modes' field IDs.",
"items": {
"type": "string"
}
}
},
"required": [
+304
View File
@@ -0,0 +1,304 @@
import { http, HttpResponse } from 'msw'
import { afterEach, describe, expect, test, vi } from 'vitest'
import { mswServer } from '@/tests/lib/msw'
vi.mock('common', async (importOriginal) => {
const actual = await importOriginal<typeof import('common')>()
return { ...actual, IS_PLATFORM: true }
})
const WIDGET_URL = 'https://api.incident.io/widget'
const API_KEY = 'test-api-key'
const STATUS_PAGE_ID = 'test-status-page-id'
function stubHappyEnv() {
vi.stubEnv('INCIDENT_IO_WIDGET_URL', WIDGET_URL)
vi.stubEnv('INCIDENT_IO_API_KEY', API_KEY)
vi.stubEnv('INCIDENT_IO_STATUS_PAGE_ID', STATUS_PAGE_ID)
}
function widgetFixture() {
return {
page_title: 'Supabase Status',
page_url: 'https://status.supabase.com',
ongoing_incidents: [
{
id: 'incident-1',
name: 'Elevated error rates',
url: 'https://status.supabase.com/incidents/incident-1',
last_update_at: '2026-01-01T00:00:00Z',
last_update_message: 'Investigating',
status: 'investigating',
current_worst_impact: 'partial_outage',
affected_components: [
{
id: 'component-1',
name: 'API',
group_name: null,
current_status: 'partial_outage',
},
],
},
],
in_progress_maintenances: [],
scheduled_maintenances: [],
}
}
afterEach(() => {
vi.unstubAllEnvs()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('getStatusPage', () => {
test('happy path: annotates the item from its linked response incident', async () => {
stubHappyEnv()
mswServer.use(
http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())),
http.get('*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents', () =>
HttpResponse.json({
incidents: [{ id: 'response-incident-1', linked_at: '2026-01-01T00:00:00Z' }],
})
),
http.get('*/v2/incidents/:id', () =>
HttpResponse.json({
incident: {
id: 'response-incident-1',
custom_field_entries: [],
},
})
)
)
const { getStatusPage } = await import('./status-page')
const result = await getStatusPage()
expect(result.isDegraded).toBe(false)
expect(result.data.ongoing_incidents[0]).toMatchObject({
id: 'incident-1',
visible: true,
show_banner: true,
})
})
test('link endpoint 404 → item gets default annotations, isDegraded stays false', async () => {
stubHappyEnv()
mswServer.use(
http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())),
http.get(
'*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents',
() => new HttpResponse(null, { status: 404 })
)
)
const { getStatusPage } = await import('./status-page')
const result = await getStatusPage()
expect(result.data.ongoing_incidents[0]).toMatchObject({
visible: true,
show_banner: true,
})
expect(result.isDegraded).toBe(false)
})
test('/v2/incidents/:id failing → item gets default annotations AND isDegraded true', async () => {
stubHappyEnv()
mswServer.use(
http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())),
http.get('*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents', () =>
HttpResponse.json({
incidents: [{ id: 'response-incident-1', linked_at: '2026-01-01T00:00:00Z' }],
})
),
http.get('*/v2/incidents/:id', () => new HttpResponse(null, { status: 500 }))
)
const { getStatusPage } = await import('./status-page')
const result = await getStatusPage()
expect(result.data.ongoing_incidents[0]).toMatchObject({
visible: true,
show_banner: true,
})
expect(result.isDegraded).toBe(true)
})
test('widget returns 503 → getStatusPage rejects', async () => {
stubHappyEnv()
mswServer.use(http.get(WIDGET_URL, () => new HttpResponse(null, { status: 503 })))
const { getStatusPage } = await import('./status-page')
await expect(getStatusPage()).rejects.toThrow()
})
test('missing API key / status page ID → succeeds with default annotations everywhere, isDegraded true', async () => {
vi.stubEnv('INCIDENT_IO_WIDGET_URL', WIDGET_URL)
vi.stubEnv('INCIDENT_IO_API_KEY', '')
vi.stubEnv('INCIDENT_IO_STATUS_PAGE_ID', '')
mswServer.use(http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())))
const { getStatusPage } = await import('./status-page')
const result = await getStatusPage()
expect(result.isDegraded).toBe(true)
expect(result.data.ongoing_incidents[0]).toMatchObject({
visible: true,
show_banner: true,
})
})
test('real payload: scheduled maintenance without starts_at/ends_at still parses', async () => {
stubHappyEnv()
const realPayload = {
page_title: 'Supabase',
page_url: 'https://statuspage.incident.io/supabase',
ongoing_incidents: [
{
name: '401 errors due to JWT rejections',
status: 'identified',
url: 'https://statuspage.incident.io/supabase/incidents/01KZZ19EQMSC9DNDA15ZGVY19V',
id: '01KZZ19EQMSC9DNDA15ZGVY19V',
current_worst_impact: 'degraded_performance',
affected_components: [
{
id: '01KJZVWMNKQ0HV8DXHRSSERV5T',
name: 'API Gateway',
current_status: 'degraded_performance',
},
],
last_update_at: '2026-09-25T16:23:02.23Z',
last_update_message:
'The deployment process changes have been rolled out across all regions. We will monitor throughout the weekend.',
},
],
in_progress_maintenances: [],
scheduled_maintenances: [
{
id: '01KWA5M6EAYR6F74N18REZNYQ1',
name: 'Scheduled Platform Maintenance 07/02',
status: 'maintenance_scheduled',
url: 'https://statuspage.incident.io/supabase/incidents/01KWA5M6EAYR6F74N18REZNYQ1',
affected_components: [],
last_update_at: '2026-06-29T16:56:35.249Z',
last_update_message: 'We will be carrying out a scheduled maintenance window...',
},
],
}
mswServer.use(
http.get(WIDGET_URL, () => HttpResponse.json(realPayload)),
http.get(
'*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents',
() => new HttpResponse(null, { status: 404 })
)
)
const { getStatusPage } = await import('./status-page')
const result = await getStatusPage()
expect(result.isDegraded).toBe(false)
expect(result.data.ongoing_incidents).toHaveLength(1)
expect(result.data.scheduled_maintenances).toHaveLength(1)
expect(result.data.scheduled_maintenances[0]).toMatchObject({
id: '01KWA5M6EAYR6F74N18REZNYQ1',
visible: true,
show_banner: true,
banner_lead_days: null,
})
expect(result.data.scheduled_maintenances[0].starts_at).toBeUndefined()
expect(result.data.scheduled_maintenances[0].ends_at).toBeUndefined()
})
})
describe('fetchWithRetry backoff (via getStatusPage widget fetch)', () => {
test('a Retry-After that parses to 0 does not skip the equal-jitter backoff floor', async () => {
stubHappyEnv()
vi.useFakeTimers()
// Equal jitter is `base / 2 + random() * (base / 2)`; pinning random() to 0 collapses it to
// the deterministic floor (base / 2) so the wait boundary below is exact, not probabilistic.
vi.spyOn(Math, 'random').mockReturnValue(0)
let widgetCallCount = 0
mswServer.use(
http.get(WIDGET_URL, () => {
widgetCallCount++
if (widgetCallCount === 1) {
return new HttpResponse(null, {
status: 429,
headers: { 'Retry-After': new Date(Date.now() - 10_000).toUTCString() },
})
}
return HttpResponse.json(widgetFixture())
}),
http.get(
'*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents',
() => new HttpResponse(null, { status: 404 })
)
)
const { getStatusPage } = await import('./status-page')
const resultPromise = getStatusPage()
await vi.advanceTimersByTimeAsync(0)
expect(widgetCallCount).toBe(1)
// Backoff floor for the first retry (attempt 0) is (1000 * 2 ** 0) / 2 = 500ms with
// Math.random() pinned to 0.
await vi.advanceTimersByTimeAsync(499)
expect(widgetCallCount).toBe(1)
// Crossing the floor triggers the retry.
await vi.advanceTimersByTimeAsync(1)
expect(widgetCallCount).toBe(2)
const result = await resultPromise
expect(result.data.ongoing_incidents[0].id).toBe('incident-1')
})
})
describe('GET /api/status-page route Cache-Control header', () => {
test('success → 300s cache with 300s stale-while-revalidate', async () => {
stubHappyEnv()
mswServer.use(
http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())),
http.get(
'*/v1/status-pages/:statusPageId/incidents/:itemId/response-incidents',
() => new HttpResponse(null, { status: 404 })
)
)
const { GET } = await import('../../app/api/status-page/route')
const response = await GET()
expect(response.headers.get('Cache-Control')).toBe(
'public, s-maxage=300, stale-while-revalidate=300'
)
})
test('degraded (missing config) → 60s cache with 60s stale-while-revalidate', async () => {
vi.stubEnv('INCIDENT_IO_WIDGET_URL', WIDGET_URL)
vi.stubEnv('INCIDENT_IO_API_KEY', '')
vi.stubEnv('INCIDENT_IO_STATUS_PAGE_ID', '')
mswServer.use(http.get(WIDGET_URL, () => HttpResponse.json(widgetFixture())))
const { GET } = await import('../../app/api/status-page/route')
const response = await GET()
expect(response.headers.get('Cache-Control')).toBe(
'public, s-maxage=60, stale-while-revalidate=60'
)
})
test('error (widget 503) → no-store', async () => {
stubHappyEnv()
mswServer.use(http.get(WIDGET_URL, () => new HttpResponse(null, { status: 503 })))
const { GET } = await import('../../app/api/status-page/route')
const response = await GET()
expect(response.headers.get('Cache-Control')).toBe('no-store')
})
})
+223
View File
@@ -0,0 +1,223 @@
import { z } from 'zod'
import customContentRaw from '@/hooks/custom-content/custom-content.json'
import { InternalServerError } from '@/lib/api/apiHelpers'
import {
annotateWidget,
mergeLinkedFlags,
type ResponseIncident,
type ResponseIncidentFlags,
} from '@/lib/api/status-page.utils'
import {
WidgetResponseSchema,
type StatusPageResponse,
type WidgetResponse,
} from '@/lib/status-page/status-page.schema'
const HIDE_BANNER_FIELD_ID = '01M3D30MY0GQ1284ERHHV4DFAT'
const BANNER_LEAD_DAYS_FIELD_ID = '01M3D332BQVSDE4ZGK86X6X69V'
const WIDGET_TIMEOUT_MS = 10_000
const LINK_TIMEOUT_MS = 5_000
const INCIDENT_TIMEOUT_MS = 5_000
const MAX_RETRIES = 2
export type StatusPageResult = { data: StatusPageResponse; isDegraded: boolean }
const LinkedResponseIncidentsSchema = z.object({
incidents: z.array(z.object({ id: z.string(), linked_at: z.string() })),
})
const CustomFieldValueSchema = z.object({
value_option: z.object({ value: z.string() }).nullish(),
value_text: z.string().nullish(),
value_numeric: z.string().nullish(),
})
const ResponseIncidentSchema = z.object({
incident: z.object({
id: z.string(),
custom_field_entries: z.array(
z.object({
custom_field: z.object({ id: z.string() }),
values: z.array(CustomFieldValueSchema),
})
),
}),
})
function parseRetryAfterMs(header: string | null): number | undefined {
if (!header) return undefined
const seconds = Number(header)
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000
const dateMs = Date.parse(header)
if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now())
return undefined
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function backoffWithJitter(attempt: number): number {
const base = 1000 * 2 ** attempt
return base / 2 + Math.random() * (base / 2)
}
// Shared by all three incident.io calls: on a 429, wait for the Retry-After hint (or an
// exponential fallback) and retry, up to MAX_RETRIES times, then return the still-429 response
// to the caller.
async function fetchWithRetry(
url: string,
init: RequestInit,
timeoutMs: number
): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
if (response.status !== 429 || attempt >= MAX_RETRIES) {
return response
}
const retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After'))
await sleep(Math.max(retryAfterMs ?? 0, backoffWithJitter(attempt)))
}
}
async function fetchWidget(url: string): Promise<WidgetResponse> {
const response = await fetchWithRetry(url, { cache: 'no-store' }, WIDGET_TIMEOUT_MS)
if (!response.ok) {
const retryAfter = response.headers.get('Retry-After') ?? undefined
const body = await response.text()
throw new InternalServerError(`incident.io Widget API responded with ${response.status}`, {
status: response.status,
body,
...(retryAfter !== undefined && { retryAfter }),
})
}
const json = await response.json()
const result = WidgetResponseSchema.safeParse(json)
if (!result.success) {
throw new InternalServerError('incident.io Widget API response did not match expected schema', {
issues: result.error.issues,
})
}
return result.data
}
async function fetchLinkedResponseIncidentIds(
apiKey: string,
statusPageId: string,
itemId: string
): Promise<Array<string>> {
const response = await fetchWithRetry(
`https://api.incident.io/v1/status-pages/${statusPageId}/incidents/${itemId}/response-incidents`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
LINK_TIMEOUT_MS
)
if (response.status === 404) return []
if (!response.ok) {
throw new Error(`incident.io response-incidents lookup responded with ${response.status}`)
}
const json = await response.json()
const result = LinkedResponseIncidentsSchema.parse(json)
return result.incidents.map((incident) => incident.id)
}
async function fetchResponseIncident(apiKey: string, id: string): Promise<ResponseIncident> {
const response = await fetchWithRetry(
`https://api.incident.io/v2/incidents/${id}`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
INCIDENT_TIMEOUT_MS
)
if (!response.ok) {
throw new Error(`incident.io incident lookup responded with ${response.status}`)
}
const json = await response.json()
return ResponseIncidentSchema.parse(json).incident
}
export async function getStatusPage(): Promise<StatusPageResult> {
const widgetUrl = process.env.INCIDENT_IO_WIDGET_URL
if (!widgetUrl) {
throw new InternalServerError('INCIDENT_IO_WIDGET_URL is not set')
}
const widget = await fetchWidget(widgetUrl)
const modeFieldIds = customContentRaw['status_page:visibility_field_ids'] ?? ['']
const apiKey = process.env.INCIDENT_IO_API_KEY
const statusPageId = process.env.INCIDENT_IO_STATUS_PAGE_ID
if (!apiKey || !statusPageId) {
console.error('INCIDENT_IO_API_KEY or INCIDENT_IO_STATUS_PAGE_ID is not set')
return { data: annotateWidget(widget, new Map(), modeFieldIds), isDegraded: true }
}
let isDegraded = false
const allItems = [
...widget.ongoing_incidents,
...widget.in_progress_maintenances,
...widget.scheduled_maintenances,
]
const linkResults = await Promise.allSettled(
allItems.map((item) => fetchLinkedResponseIncidentIds(apiKey, statusPageId, item.id))
)
const linkedIdsByItemId = new Map<string, Array<string>>()
linkResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
linkedIdsByItemId.set(allItems[index].id, result.value)
} else {
isDegraded = true
}
})
const uniqueLinkedIds = Array.from(new Set(Array.from(linkedIdsByItemId.values()).flat()))
const incidentResults = await Promise.allSettled(
uniqueLinkedIds.map((id) => fetchResponseIncident(apiKey, id))
)
const incidentsById = new Map<string, ResponseIncident>()
incidentResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
incidentsById.set(uniqueLinkedIds[index], result.value)
} else {
isDegraded = true
}
})
const flagsById = new Map<string, ResponseIncidentFlags>()
for (const item of allItems) {
const linkedIncidents = (linkedIdsByItemId.get(item.id) ?? [])
.map((id) => incidentsById.get(id))
.filter((incident): incident is ResponseIncident => incident !== undefined)
if (linkedIncidents.length > 0) {
flagsById.set(
item.id,
mergeLinkedFlags(linkedIncidents, modeFieldIds, {
hideBannerFieldId: HIDE_BANNER_FIELD_ID,
leadDaysFieldId: BANNER_LEAD_DAYS_FIELD_ID,
})
)
}
}
return { data: annotateWidget(widget, flagsById, modeFieldIds), isDegraded }
}
@@ -0,0 +1,319 @@
import { describe, expect, test } from 'vitest'
import {
annotateWidget,
computeAnnotations,
mergeLinkedFlags,
readCustomFieldFlag,
readCustomFieldNumber,
type CustomFieldEntry,
type ResponseIncident,
type ResponseIncidentFlags,
} from './status-page.utils'
import type { WidgetResponse } from '@/lib/status-page/status-page.schema'
const FIELD_ID = 'field-1'
const OTHER_FIELD_ID = 'field-2'
function entriesWith(fieldId: string, values: CustomFieldEntry['values']): Array<CustomFieldEntry> {
return [{ custom_field: { id: fieldId }, values }]
}
describe('readCustomFieldFlag', () => {
test('matches via value_option', () => {
const entries = entriesWith(FIELD_ID, [{ value_option: { value: 'true' } }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(true)
})
test('matches via value_text', () => {
const entries = entriesWith(FIELD_ID, [{ value_text: 'true' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(true)
})
test('matches via value_numeric (non-zero → true)', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '5' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(true)
})
test('value_numeric of 0 → false', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '0' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(false)
})
test('value_numeric negative → false', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '-5' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(false)
})
test('missing entry → false', () => {
const entries = entriesWith(OTHER_FIELD_ID, [{ value_text: 'true' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(false)
})
test('empty fieldId → false (no-op)', () => {
const entries = entriesWith(FIELD_ID, [{ value_text: 'true' }])
expect(readCustomFieldFlag(entries, '')).toBe(false)
})
test('is case-insensitive', () => {
const entries = entriesWith(FIELD_ID, [{ value_option: { value: 'TRUE' } }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(true)
const entries2 = entriesWith(FIELD_ID, [{ value_text: 'Yes' }])
expect(readCustomFieldFlag(entries2, FIELD_ID)).toBe(true)
})
test('trims whitespace', () => {
const entries = entriesWith(FIELD_ID, [{ value_text: ' true ' }])
expect(readCustomFieldFlag(entries, FIELD_ID)).toBe(true)
})
})
describe('readCustomFieldNumber', () => {
test('reads a valid positive value via value_numeric', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '7' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(7)
})
test('reads a valid positive value via value_option', () => {
const entries = entriesWith(FIELD_ID, [{ value_option: { value: '3' } }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(3)
})
test('reads a valid positive value via value_text', () => {
const entries = entriesWith(FIELD_ID, [{ value_text: '10' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(10)
})
test('0 → null', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '0' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(null)
})
test('negative number → null', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '-5' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(null)
})
test('non-numeric string (NaN) → null', () => {
const entries = entriesWith(FIELD_ID, [{ value_text: 'not-a-number' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(null)
})
test('empty fieldId → null', () => {
const entries = entriesWith(FIELD_ID, [{ value_numeric: '7' }])
expect(readCustomFieldNumber(entries, '')).toBe(null)
})
test('missing entry → null', () => {
const entries = entriesWith(OTHER_FIELD_ID, [{ value_numeric: '7' }])
expect(readCustomFieldNumber(entries, FIELD_ID)).toBe(null)
})
})
describe('mergeLinkedFlags', () => {
const fieldIds = { hideBannerFieldId: 'hide-banner', leadDaysFieldId: 'lead-days' }
test('OR-merges modeFlags across multiple linked incidents', () => {
const incidentA: ResponseIncident = {
id: 'a',
custom_field_entries: entriesWith('mode-1', [{ value_text: 'true' }]),
}
const incidentB: ResponseIncident = {
id: 'b',
custom_field_entries: entriesWith('mode-1', [{ value_text: 'false' }]),
}
const result = mergeLinkedFlags([incidentA, incidentB], ['mode-1'], fieldIds)
expect(result.modeFlags['mode-1']).toBe(true)
})
test('AND-merges hideBanner: all must have it set for it to end up true', () => {
const incidentA: ResponseIncident = {
id: 'a',
custom_field_entries: entriesWith('hide-banner', [{ value_text: 'true' }]),
}
const incidentB: ResponseIncident = {
id: 'b',
custom_field_entries: entriesWith('hide-banner', [{ value_text: 'true' }]),
}
const result = mergeLinkedFlags([incidentA, incidentB], [], fieldIds)
expect(result.hideBanner).toBe(true)
})
test('one incident missing hideBanner brings the merge to false', () => {
const incidentA: ResponseIncident = {
id: 'a',
custom_field_entries: entriesWith('hide-banner', [{ value_text: 'true' }]),
}
const incidentB: ResponseIncident = {
id: 'b',
custom_field_entries: entriesWith('hide-banner', [{ value_text: 'false' }]),
}
const result = mergeLinkedFlags([incidentA, incidentB], [], fieldIds)
expect(result.hideBanner).toBe(false)
})
test('leadDays picks the max of the non-null values across incidents', () => {
const incidentA: ResponseIncident = {
id: 'a',
custom_field_entries: entriesWith('lead-days', [{ value_numeric: '3' }]),
}
const incidentB: ResponseIncident = {
id: 'b',
custom_field_entries: entriesWith('lead-days', [{ value_numeric: '10' }]),
}
const result = mergeLinkedFlags([incidentA, incidentB], [], fieldIds)
expect(result.leadDays).toBe(10)
})
test('leadDays is null when none set it', () => {
const incidentA: ResponseIncident = { id: 'a', custom_field_entries: [] }
const result = mergeLinkedFlags([incidentA], [], fieldIds)
expect(result.leadDays).toBe(null)
})
})
describe('computeAnnotations', () => {
test('flags undefined → all-permissive defaults', () => {
expect(computeAnnotations(undefined, [''])).toEqual({
visible: true,
show_banner: true,
banner_lead_days: null,
})
})
test('hideBanner true → show_banner false', () => {
const flags: ResponseIncidentFlags = { hideBanner: true, modeFlags: {}, leadDays: null }
expect(computeAnnotations(flags, ['']).show_banner).toBe(false)
})
test('hideBanner false → show_banner true', () => {
const flags: ResponseIncidentFlags = { hideBanner: false, modeFlags: {}, leadDays: null }
expect(computeAnnotations(flags, ['']).show_banner).toBe(true)
})
test('own mode ID empty + another mode flag set → hidden', () => {
const flags: ResponseIncidentFlags = {
hideBanner: false,
modeFlags: { 'other-mode': true },
leadDays: null,
}
expect(computeAnnotations(flags, ['', 'other-mode']).visible).toBe(false)
})
test('own mode ID set + own flag true, even if another is also true → visible', () => {
const flags: ResponseIncidentFlags = {
hideBanner: false,
modeFlags: { 'own-mode': true, 'other-mode': true },
leadDays: null,
}
expect(computeAnnotations(flags, ['own-mode', 'other-mode']).visible).toBe(true)
})
test('own mode ID set + own flag false + another flag true → hidden', () => {
const flags: ResponseIncidentFlags = {
hideBanner: false,
modeFlags: { 'own-mode': false, 'other-mode': true },
leadDays: null,
}
expect(computeAnnotations(flags, ['own-mode', 'other-mode']).visible).toBe(false)
})
test('nothing set anywhere → visible', () => {
const flags: ResponseIncidentFlags = {
hideBanner: false,
modeFlags: { 'own-mode': false, 'other-mode': false },
leadDays: null,
}
expect(computeAnnotations(flags, ['own-mode', 'other-mode']).visible).toBe(true)
})
})
describe('annotateWidget', () => {
const baseIncident = {
id: 'incident-1',
name: 'Incident 1',
url: 'https://status.example.com/incidents/incident-1',
last_update_at: '2026-01-01T00:00:00Z',
last_update_message: 'Investigating',
affected_components: [],
status: 'investigating' as const,
current_worst_impact: 'partial_outage' as const,
}
const baseInProgressMaintenance = {
id: 'maintenance-1',
name: 'Maintenance 1',
url: 'https://status.example.com/maintenances/maintenance-1',
last_update_at: '2026-01-01T00:00:00Z',
last_update_message: null,
affected_components: [],
status: 'maintenance_in_progress' as const,
started_at: '2026-01-01T00:00:00Z',
scheduled_end_at: null,
}
const baseScheduledMaintenance = {
id: 'scheduled-1',
name: 'Scheduled 1',
url: 'https://status.example.com/maintenances/scheduled-1',
last_update_at: '2026-01-01T00:00:00Z',
last_update_message: null,
affected_components: [],
status: 'maintenance_scheduled' as const,
starts_at: '2026-02-01T00:00:00Z',
ends_at: '2026-02-02T00:00:00Z',
}
const widget: WidgetResponse = {
page_title: 'Supabase Status',
page_url: 'https://status.example.com',
ongoing_incidents: [baseIncident],
in_progress_maintenances: [baseInProgressMaintenance],
scheduled_maintenances: [baseScheduledMaintenance],
}
test('preserves non-annotation fields, and gates visibility/annotations per item', () => {
const flagsById = new Map<string, ResponseIncidentFlags>([
['incident-1', { hideBanner: true, modeFlags: {}, leadDays: 5 }],
])
const result = annotateWidget(widget, flagsById, [''])
expect(result.page_title).toBe('Supabase Status')
expect(result.page_url).toBe('https://status.example.com')
expect(result.ongoing_incidents[0]).toMatchObject({
...baseIncident,
visible: true,
show_banner: false,
})
expect(result.ongoing_incidents[0]).not.toHaveProperty('banner_lead_days')
expect(result.in_progress_maintenances[0]).not.toHaveProperty('banner_lead_days')
expect(result.scheduled_maintenances[0]).toHaveProperty('banner_lead_days')
})
test('an item absent from flagsById gets the full default annotations', () => {
const result = annotateWidget(widget, new Map(), [''])
expect(result.ongoing_incidents[0]).toMatchObject({
visible: true,
show_banner: true,
})
expect(result.in_progress_maintenances[0]).toMatchObject({
visible: true,
show_banner: true,
})
expect(result.scheduled_maintenances[0]).toMatchObject({
visible: true,
show_banner: true,
banner_lead_days: null,
})
})
})
+138
View File
@@ -0,0 +1,138 @@
import type { StatusPageResponse, WidgetResponse } from '@/lib/status-page/status-page.schema'
export type CustomFieldValue = {
value_option?: { value: string } | null
value_text?: string | null
value_numeric?: string | null
}
export type CustomFieldEntry = {
custom_field: { id: string }
values: Array<CustomFieldValue>
}
export type ResponseIncident = {
id: string
custom_field_entries: Array<CustomFieldEntry>
}
const TRUE_VALUES = new Set(['yes', 'true', '1'])
export function readCustomFieldFlag(entries: Array<CustomFieldEntry>, fieldId: string): boolean {
if (!fieldId) return false
const entry = entries.find((e) => e.custom_field.id === fieldId)
if (!entry) return false
return entry.values.some((value) => {
const text = (value.value_option?.value ?? value.value_text ?? '').trim().toLowerCase()
if (TRUE_VALUES.has(text)) return true
if (value.value_numeric != null) {
const numeric = Number(value.value_numeric)
return Number.isFinite(numeric) && numeric > 0
}
return false
})
}
export function readCustomFieldNumber(
entries: Array<CustomFieldEntry>,
fieldId: string
): number | null {
if (!fieldId) return null
const entry = entries.find((e) => e.custom_field.id === fieldId)
if (!entry) return null
for (const value of entry.values) {
const raw = value.value_numeric ?? value.value_option?.value ?? value.value_text
if (raw == null) continue
const numeric = Number(raw)
if (Number.isFinite(numeric) && numeric > 0) return numeric
}
return null
}
export type ResponseIncidentFlags = {
hideBanner: boolean
modeFlags: Record<string, boolean>
leadDays: number | null
}
export type ResponseIncidentFieldIds = {
hideBannerFieldId: string
leadDaysFieldId: string
}
export function mergeLinkedFlags(
incidents: Array<ResponseIncident>,
modeFieldIds: Array<string>,
fieldIds: ResponseIncidentFieldIds
): ResponseIncidentFlags {
const modeFlags: Record<string, boolean> = {}
for (const fieldId of modeFieldIds.filter(Boolean)) {
modeFlags[fieldId] = incidents.some((incident) =>
readCustomFieldFlag(incident.custom_field_entries, fieldId)
)
}
const hideBanner = incidents.every((incident) =>
readCustomFieldFlag(incident.custom_field_entries, fieldIds.hideBannerFieldId)
)
const leadDaysValues = incidents
.map((incident) =>
readCustomFieldNumber(incident.custom_field_entries, fieldIds.leadDaysFieldId)
)
.filter((value): value is number => value !== null)
return {
hideBanner,
modeFlags,
leadDays: leadDaysValues.length > 0 ? Math.max(...leadDaysValues) : null,
}
}
export function computeAnnotations(
flags: ResponseIncidentFlags | undefined,
modeFieldIds: Array<string>
): { visible: boolean; show_banner: boolean; banner_lead_days: number | null } {
if (!flags) {
return { visible: true, show_banner: true, banner_lead_days: null }
}
const [own = ''] = modeFieldIds
const anySet = modeFieldIds.filter(Boolean).some((id) => flags.modeFlags[id])
const visible = !anySet || (own !== '' && flags.modeFlags[own] === true)
return {
visible,
show_banner: !flags.hideBanner,
banner_lead_days: flags.leadDays,
}
}
export function annotateWidget(
widget: WidgetResponse,
flagsById: Map<string, ResponseIncidentFlags>,
modeFieldIds: Array<string>
): StatusPageResponse {
const withBaseAnnotations = <T extends { id: string }>(item: T) => {
const { visible, show_banner } = computeAnnotations(flagsById.get(item.id), modeFieldIds)
return { ...item, visible, show_banner }
}
return {
...widget,
ongoing_incidents: widget.ongoing_incidents.map(withBaseAnnotations),
in_progress_maintenances: widget.in_progress_maintenances.map(withBaseAnnotations),
scheduled_maintenances: widget.scheduled_maintenances.map((item) => {
const annotations = computeAnnotations(flagsById.get(item.id), modeFieldIds)
return { ...item, ...annotations }
}),
}
}
+1
View File
@@ -23,6 +23,7 @@ export const HOSTED_SUPPORTED_API_URLS = [
'/generate-attachment-url',
'/incident-status',
'/incident-banner',
'/status-page',
'/status-override',
'/api/integrations/stripe-sync',
'/content/graphql',
@@ -0,0 +1,93 @@
import { describe, expect, expectTypeOf, test, vi } from 'vitest'
import { z } from 'zod'
import {
WidgetIncidentSchema,
WidgetInProgressMaintenanceSchema,
WidgetResponseSchema,
WidgetScheduledMaintenanceSchema,
} from './status-page.schema'
describe('WidgetResponseSchema: tolerant arrays (fail-open)', () => {
test('drops a malformed item from one array without failing the whole parse, leaving the valid item in that array and the other arrays intact', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const payload = {
page_title: 'Supabase',
page_url: 'https://statuspage.incident.io/supabase',
ongoing_incidents: [
{
id: 'incident-valid',
name: 'Valid incident',
url: 'https://status.example.com/incidents/incident-valid',
last_update_at: '2026-01-01T00:00:00Z',
status: 'identified',
current_worst_impact: 'partial_outage',
},
// Malformed: missing required `id`/`name`/`url`/`last_update_at`.
{ status: 'identified' },
],
in_progress_maintenances: [
{
id: 'maintenance-valid',
name: 'Valid maintenance',
url: 'https://status.example.com/maintenances/maintenance-valid',
last_update_at: '2026-01-01T00:00:00Z',
status: 'maintenance_in_progress',
started_at: '2026-01-01T00:00:00Z',
},
],
scheduled_maintenances: [],
}
const result = WidgetResponseSchema.safeParse(payload)
expect(result.success).toBe(true)
if (!result.success) return
// The other valid item in the same array survives...
expect(result.data.ongoing_incidents).toHaveLength(1)
expect(result.data.ongoing_incidents[0].id).toBe('incident-valid')
// ...and the other arrays are unaffected by the malformed item elsewhere.
expect(result.data.in_progress_maintenances).toHaveLength(1)
expect(result.data.in_progress_maintenances[0].id).toBe('maintenance-valid')
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('ongoing_incidents'),
expect.any(Object)
)
warnSpy.mockRestore()
})
test('defaults a missing array key to an empty array', () => {
const result = WidgetResponseSchema.safeParse({
page_title: 'Supabase',
page_url: 'https://statuspage.incident.io/supabase',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.data.ongoing_incidents).toEqual([])
expect(result.data.in_progress_maintenances).toEqual([])
expect(result.data.scheduled_maintenances).toEqual([])
})
test('tolerantArray keeps parsed item types concrete, not any', () => {
const result = WidgetResponseSchema.parse({
page_title: 'Supabase',
page_url: 'https://statuspage.incident.io/supabase',
})
expectTypeOf(result.ongoing_incidents).toEqualTypeOf<
Array<z.infer<typeof WidgetIncidentSchema>>
>()
expectTypeOf(result.in_progress_maintenances).toEqualTypeOf<
Array<z.infer<typeof WidgetInProgressMaintenanceSchema>>
>()
expectTypeOf(result.scheduled_maintenances).toEqualTypeOf<
Array<z.infer<typeof WidgetScheduledMaintenanceSchema>>
>()
})
})
@@ -0,0 +1,96 @@
import { z } from 'zod'
const ComponentStatus = z.enum([
'operational',
'degraded_performance',
'partial_outage',
'full_outage',
])
const WorstImpact = z.enum(['degraded_performance', 'partial_outage', 'full_outage'])
export const AffectedComponentSchema = z.object({
id: z.string(),
name: z.string(),
group_name: z.string().nullish(),
current_status: ComponentStatus.catch('degraded_performance'),
})
const Base = {
id: z.string(),
name: z.string(),
url: z.string(),
last_update_at: z.string(),
last_update_message: z.string().nullish(),
affected_components: z.array(AffectedComponentSchema).default([]),
}
export const WidgetIncidentSchema = z.object({
...Base,
status: z.enum(['investigating', 'identified', 'monitoring']).catch('investigating'),
current_worst_impact: WorstImpact.catch('full_outage'),
})
export const WidgetInProgressMaintenanceSchema = z.object({
...Base,
status: z.literal('maintenance_in_progress').catch('maintenance_in_progress'),
started_at: z.string(),
scheduled_end_at: z.string().nullish(),
})
export const WidgetScheduledMaintenanceSchema = z.object({
...Base,
status: z.literal('maintenance_scheduled').catch('maintenance_scheduled'),
starts_at: z.string().nullish(),
ends_at: z.string().nullish(),
})
function tolerantArray<T extends z.ZodTypeAny>(itemSchema: T, arrayName: string) {
return z
.array(z.unknown())
.default([])
.transform(
(items): Array<z.output<T>> =>
items.flatMap((item) => {
const result = itemSchema.safeParse(item)
if (result.success) return [result.data]
const rawId =
typeof item === 'object' && item !== null && 'id' in item
? (item as { id?: unknown }).id
: undefined
console.warn(`incident.io Widget API: dropping malformed item in ${arrayName}`, {
id: rawId,
issues: result.error.issues,
})
return []
})
)
}
export const WidgetResponseSchema = z.object({
page_title: z.string(),
page_url: z.string(),
ongoing_incidents: tolerantArray(WidgetIncidentSchema, 'ongoing_incidents'),
in_progress_maintenances: tolerantArray(
WidgetInProgressMaintenanceSchema,
'in_progress_maintenances'
),
scheduled_maintenances: tolerantArray(WidgetScheduledMaintenanceSchema, 'scheduled_maintenances'),
})
const Annotations = { visible: z.boolean(), show_banner: z.boolean() }
export const StatusPageResponseSchema = WidgetResponseSchema.extend({
ongoing_incidents: z.array(WidgetIncidentSchema.extend(Annotations)),
in_progress_maintenances: z.array(WidgetInProgressMaintenanceSchema.extend(Annotations)),
scheduled_maintenances: z.array(
WidgetScheduledMaintenanceSchema.extend({
...Annotations,
banner_lead_days: z.number().positive().nullable(),
})
),
})
export type WidgetResponse = z.infer<typeof WidgetResponseSchema>
export type StatusPageResponse = z.infer<typeof StatusPageResponseSchema>
export type AffectedComponent = z.infer<typeof AffectedComponentSchema>
+21
View File
@@ -27,6 +27,7 @@ import { Route as OrgChar91_Char93RouteImport } from './routes/org.[_]'
import { Route as NewSlugRouteImport } from './routes/new/$slug'
import { Route as McpSecretsRouteImport } from './routes/mcp/secrets'
import { Route as IntegrationsVercelRouteImport } from './routes/integrations/vercel'
import { Route as ApiStatusPageRouteImport } from './routes/api/status-page'
import { Route as ApiStatusOverrideRouteImport } from './routes/api/status-override'
import { Route as ApiScopedAccessTokenPermissionsRouteImport } from './routes/api/scoped-access-token-permissions'
import { Route as ApiParseQueryRouteImport } from './routes/api/parse-query'
@@ -422,6 +423,11 @@ const IntegrationsVercelRoute = IntegrationsVercelRouteImport.update({
path: '/integrations/vercel',
getParentRoute: () => rootRouteImport,
} as any)
const ApiStatusPageRoute = ApiStatusPageRouteImport.update({
id: '/api/status-page',
path: '/api/status-page',
getParentRoute: () => rootRouteImport,
} as any)
const ApiStatusOverrideRoute = ApiStatusOverrideRouteImport.update({
id: '/api/status-override',
path: '/api/status-override',
@@ -2175,6 +2181,7 @@ export interface FileRoutesByFullPath {
'/api/parse-query': typeof ApiParseQueryRoute
'/api/scoped-access-token-permissions': typeof ApiScopedAccessTokenPermissionsRoute
'/api/status-override': typeof ApiStatusOverrideRoute
'/api/status-page': typeof ApiStatusPageRoute
'/integrations/vercel': typeof IntegrationsVercelRouteWithChildren
'/mcp/secrets': typeof McpSecretsRoute
'/new/$slug': typeof NewSlugRoute
@@ -2497,6 +2504,7 @@ export interface FileRoutesByTo {
'/api/parse-query': typeof ApiParseQueryRoute
'/api/scoped-access-token-permissions': typeof ApiScopedAccessTokenPermissionsRoute
'/api/status-override': typeof ApiStatusOverrideRoute
'/api/status-page': typeof ApiStatusPageRoute
'/integrations/vercel': typeof IntegrationsVercelRouteWithChildren
'/mcp/secrets': typeof McpSecretsRoute
'/new/$slug': typeof NewSlugRoute
@@ -2809,6 +2817,7 @@ export interface FileRoutesById {
'/api/parse-query': typeof ApiParseQueryRoute
'/api/scoped-access-token-permissions': typeof ApiScopedAccessTokenPermissionsRoute
'/api/status-override': typeof ApiStatusOverrideRoute
'/api/status-page': typeof ApiStatusPageRoute
'/integrations/vercel': typeof IntegrationsVercelRouteWithChildren
'/mcp/secrets': typeof McpSecretsRoute
'/new/$slug': typeof NewSlugRoute
@@ -3134,6 +3143,7 @@ export interface FileRouteTypes {
| '/api/parse-query'
| '/api/scoped-access-token-permissions'
| '/api/status-override'
| '/api/status-page'
| '/integrations/vercel'
| '/mcp/secrets'
| '/new/$slug'
@@ -3456,6 +3466,7 @@ export interface FileRouteTypes {
| '/api/parse-query'
| '/api/scoped-access-token-permissions'
| '/api/status-override'
| '/api/status-page'
| '/integrations/vercel'
| '/mcp/secrets'
| '/new/$slug'
@@ -3767,6 +3778,7 @@ export interface FileRouteTypes {
| '/api/parse-query'
| '/api/scoped-access-token-permissions'
| '/api/status-override'
| '/api/status-page'
| '/integrations/vercel'
| '/mcp/secrets'
| '/new/$slug'
@@ -4081,6 +4093,7 @@ export interface RootRouteChildren {
ApiParseQueryRoute: typeof ApiParseQueryRoute
ApiScopedAccessTokenPermissionsRoute: typeof ApiScopedAccessTokenPermissionsRoute
ApiStatusOverrideRoute: typeof ApiStatusOverrideRoute
ApiStatusPageRoute: typeof ApiStatusPageRoute
IntegrationsVercelRoute: typeof IntegrationsVercelRouteWithChildren
McpSecretsRoute: typeof McpSecretsRoute
NewSlugRoute: typeof NewSlugRoute
@@ -4309,6 +4322,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IntegrationsVercelRouteImport
parentRoute: typeof rootRouteImport
}
'/api/status-page': {
id: '/api/status-page'
path: '/api/status-page'
fullPath: '/api/status-page'
preLoaderRoute: typeof ApiStatusPageRouteImport
parentRoute: typeof rootRouteImport
}
'/api/status-override': {
id: '/api/status-override'
path: '/api/status-override'
@@ -7207,6 +7227,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiParseQueryRoute: ApiParseQueryRoute,
ApiScopedAccessTokenPermissionsRoute: ApiScopedAccessTokenPermissionsRoute,
ApiStatusOverrideRoute: ApiStatusOverrideRoute,
ApiStatusPageRoute: ApiStatusPageRoute,
IntegrationsVercelRoute: IntegrationsVercelRouteWithChildren,
McpSecretsRoute: McpSecretsRoute,
NewSlugRoute: NewSlugRoute,
+13
View File
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'
import { GET, HEAD, OPTIONS } from '@/app/api/status-page/route'
export const Route = createFileRoute('/api/status-page')({
server: {
handlers: {
GET: () => GET(),
HEAD: () => HEAD(),
OPTIONS: () => OPTIONS(),
},
},
})
+2
View File
@@ -105,6 +105,8 @@
"STATUSPAGE_API_KEY",
"STATUSPAGE_PAGE_ID",
"INCIDENT_IO_API_KEY",
"INCIDENT_IO_WIDGET_URL",
"INCIDENT_IO_STATUS_PAGE_ID",
"LIVE_SUPABASE_SECRET_KEY",
// Selects the build mode for the studio's `build`/`start` scripts
// (e.g. e2e sets `MODE=test`). Listed so turbo invalidates the