Files
CharisandClaude Code b2cf3693dd 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>
2026-09-28 11:36:29 -04:00

66 lines
2.0 KiB
TypeScript

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 }
)
}
}