From 8307ce6db7a729948eacc98f689840402f0a1a70 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 18:04:12 +0000 Subject: [PATCH 1/9] use incident io status page Co-authored-by: Jakub Dobry --- .env.example | 4 +- .../layouts/status-indicator.server.tsx | 40 ++++------- .../dashboard/layouts/status-indicator.ts | 66 +++++++++++++++++++ src/lib/env.ts | 2 + tests/unit/status-indicator.test.ts | 62 +++++++++++++++++ 5 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 src/features/dashboard/layouts/status-indicator.ts create mode 100644 tests/unit/status-indicator.test.ts diff --git a/.env.example b/.env.example index 9a99dd6fc..755bf6ada 100644 --- a/.env.example +++ b/.env.example @@ -68,8 +68,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key # NEXT_PUBLIC_INCLUDE_REPORT_ISSUE=0 ### Enable dashboard status indicator feature: set to 1 to enable -### When enabled, the E2B status is read from https://status.e2b.dev +### When enabled, the E2B status is read from the incident.io widget API # NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR=0 +# NEXT_PUBLIC_STATUS_PAGE_URL=https://status.e2b.dev +# NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL=https://status.e2b.dev/api/widget ### Set to 1 to use mock data # NEXT_PUBLIC_MOCK_DATA=0 diff --git a/src/features/dashboard/layouts/status-indicator.server.tsx b/src/features/dashboard/layouts/status-indicator.server.tsx index 1eed06e30..d592c8677 100644 --- a/src/features/dashboard/layouts/status-indicator.server.tsx +++ b/src/features/dashboard/layouts/status-indicator.server.tsx @@ -4,41 +4,25 @@ import { cacheLife } from 'next/cache' import Link from 'next/link' import { l } from '@/core/shared/clients/logger/logger' import { LiveDot } from '@/ui/live' +import { + type AggregateState, + getStatusPageStateFromWidget, + getStatusPageUrl, + getStatusPageWidgetUrl, + type IncidentIOWidgetResponse, +} from './status-indicator' -const STATUS_PAGE_URL = 'https://status.e2b.dev' -const STATUS_PAGE_INDEX_URL = `${STATUS_PAGE_URL}/index.json` +export const STATUS_PAGE_URL = getStatusPageUrl() +const STATUS_PAGE_WIDGET_URL = getStatusPageWidgetUrl(STATUS_PAGE_URL) const STATUS_PAGE_FETCH_TIMEOUT_MS = 5_000 const STATUS_PAGE_CACHE_SECONDS = 300 -type AggregateState = - | 'operational' - | 'degraded' - | 'downtime' - | 'maintenance' - | 'unknown' - -interface StatusPageIndexResponse { - data?: { - attributes?: { - aggregate_state?: string - } - } -} - interface StatusUI { label: string dotCircleClassName: string dotClassName: string } -function toAggregateState(value: string | undefined): AggregateState { - if (value === 'operational') return 'operational' - if (value === 'degraded') return 'degraded' - if (value === 'downtime') return 'downtime' - if (value === 'maintenance') return 'maintenance' - return 'unknown' -} - function getStatusUI(state: AggregateState): StatusUI { switch (state) { case 'operational': @@ -83,7 +67,7 @@ async function getStatusPageState(): Promise { }) try { - const response = await fetch(STATUS_PAGE_INDEX_URL, { + const response = await fetch(STATUS_PAGE_WIDGET_URL, { cache: 'force-cache', next: { revalidate: STATUS_PAGE_CACHE_SECONDS }, signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS), @@ -101,8 +85,8 @@ async function getStatusPageState(): Promise { return 'unknown' } - const data = (await response.json()) as StatusPageIndexResponse - return toAggregateState(data.data?.attributes?.aggregate_state) + const data = (await response.json()) as IncidentIOWidgetResponse + return getStatusPageStateFromWidget(data) } catch { return 'unknown' } diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts new file mode 100644 index 000000000..bbc9b9f7e --- /dev/null +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -0,0 +1,66 @@ +export type AggregateState = + | 'operational' + | 'degraded' + | 'downtime' + | 'maintenance' + | 'unknown' + +export interface IncidentIOWidgetEvent { + affected_components?: Array<{ + status?: string + }> +} + +export interface IncidentIOWidgetResponse { + ongoing_incidents?: IncidentIOWidgetEvent[] + in_progress_maintenances?: IncidentIOWidgetEvent[] + scheduled_maintenances?: IncidentIOWidgetEvent[] +} + +export function getStatusPageUrl() { + return (process.env.NEXT_PUBLIC_STATUS_PAGE_URL ?? 'https://status.e2b.dev') + .trim() + .replace(/\/+$/, '') +} + +export function getStatusPageWidgetUrl(statusPageUrl: string) { + const configuredWidgetUrl = + process.env.NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL?.trim() + + if (configuredWidgetUrl) return configuredWidgetUrl + + return `${statusPageUrl}/api/widget` +} + +function hasEvents(events: IncidentIOWidgetEvent[] | undefined) { + return Array.isArray(events) && events.length > 0 +} + +function getWorstComponentState( + events: IncidentIOWidgetEvent[] | undefined +): AggregateState | undefined { + const componentStatuses = + events?.flatMap( + (event) => + event.affected_components?.map((component) => component.status) ?? [] + ) ?? [] + + if (componentStatuses.includes('full_outage')) return 'downtime' + if (componentStatuses.includes('partial_outage')) return 'degraded' + if (componentStatuses.includes('degraded_performance')) return 'degraded' + if (componentStatuses.includes('under_maintenance')) return 'maintenance' + + return undefined +} + +export function getStatusPageStateFromWidget( + data: IncidentIOWidgetResponse +): AggregateState { + if (hasEvents(data.ongoing_incidents)) { + return getWorstComponentState(data.ongoing_incidents) ?? 'degraded' + } + + if (hasEvents(data.in_progress_maintenances)) return 'maintenance' + + return 'operational' +} diff --git a/src/lib/env.ts b/src/lib/env.ts index 75f054c65..0dad5ad00 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -55,6 +55,8 @@ export const clientSchema = z.object({ NEXT_PUBLIC_INCLUDE_ARGUS: z.string().optional(), NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(), NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR: z.string().optional(), + NEXT_PUBLIC_STATUS_PAGE_URL: z.url().optional(), + NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL: z.url().optional(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(), NEXT_PUBLIC_SCAN: z.string().optional(), NEXT_PUBLIC_MOCK_DATA: z.string().optional(), diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts new file mode 100644 index 000000000..3a247b77a --- /dev/null +++ b/tests/unit/status-indicator.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest' +import { getStatusPageStateFromWidget } from '@/features/dashboard/layouts/status-indicator' + +describe('status-indicator', () => { + it('should report operational when widget has no active events', () => { + expect( + getStatusPageStateFromWidget({ + ongoing_incidents: [], + in_progress_maintenances: [], + scheduled_maintenances: [ + { + affected_components: [{ status: 'under_maintenance' }], + }, + ], + }) + ).toBe('operational') + }) + + it('should report maintenance for in-progress maintenances', () => { + expect( + getStatusPageStateFromWidget({ + ongoing_incidents: [], + in_progress_maintenances: [{}], + }) + ).toBe('maintenance') + }) + + it('should report downtime for full outage incidents', () => { + expect( + getStatusPageStateFromWidget({ + ongoing_incidents: [ + { + affected_components: [ + { status: 'degraded_performance' }, + { status: 'full_outage' }, + ], + }, + ], + }) + ).toBe('downtime') + }) + + it('should report degraded for partial outage incidents', () => { + expect( + getStatusPageStateFromWidget({ + ongoing_incidents: [ + { + affected_components: [{ status: 'partial_outage' }], + }, + ], + }) + ).toBe('degraded') + }) + + it('should report degraded when incident has no component status', () => { + expect( + getStatusPageStateFromWidget({ + ongoing_incidents: [{}], + }) + ).toBe('degraded') + }) +}) From 25a5a8b35382cb8ef85d28dbca3fd7b84f81f7ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 18:11:01 +0000 Subject: [PATCH 2/9] use new incident io page Co-authored-by: Jakub Dobry --- .env.example | 6 +- .../layouts/status-indicator.server.tsx | 14 ++-- .../dashboard/layouts/status-indicator.ts | 75 +++++++++++-------- src/lib/env.ts | 2 +- tests/unit/status-indicator.test.ts | 62 +++++++-------- 5 files changed, 84 insertions(+), 75 deletions(-) diff --git a/.env.example b/.env.example index 755bf6ada..7069993d9 100644 --- a/.env.example +++ b/.env.example @@ -68,10 +68,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key # NEXT_PUBLIC_INCLUDE_REPORT_ISSUE=0 ### Enable dashboard status indicator feature: set to 1 to enable -### When enabled, the E2B status is read from the incident.io widget API +### When enabled, the E2B status is read from the incident.io summary API # NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR=0 -# NEXT_PUBLIC_STATUS_PAGE_URL=https://status.e2b.dev -# NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL=https://status.e2b.dev/api/widget +# NEXT_PUBLIC_STATUS_PAGE_URL=https://statuspage.incident.io/e2b-service +# NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL=https://statuspage.incident.io/e2b-service/api/v2/summary.json ### Set to 1 to use mock data # NEXT_PUBLIC_MOCK_DATA=0 diff --git a/src/features/dashboard/layouts/status-indicator.server.tsx b/src/features/dashboard/layouts/status-indicator.server.tsx index d592c8677..912e47f0d 100644 --- a/src/features/dashboard/layouts/status-indicator.server.tsx +++ b/src/features/dashboard/layouts/status-indicator.server.tsx @@ -6,14 +6,14 @@ import { l } from '@/core/shared/clients/logger/logger' import { LiveDot } from '@/ui/live' import { type AggregateState, - getStatusPageStateFromWidget, + getStatusPageStateFromSummary, + getStatusPageSummaryUrl, getStatusPageUrl, - getStatusPageWidgetUrl, - type IncidentIOWidgetResponse, + type IncidentIOStatusPageSummaryResponse, } from './status-indicator' export const STATUS_PAGE_URL = getStatusPageUrl() -const STATUS_PAGE_WIDGET_URL = getStatusPageWidgetUrl(STATUS_PAGE_URL) +const STATUS_PAGE_SUMMARY_URL = getStatusPageSummaryUrl(STATUS_PAGE_URL) const STATUS_PAGE_FETCH_TIMEOUT_MS = 5_000 const STATUS_PAGE_CACHE_SECONDS = 300 @@ -67,7 +67,7 @@ async function getStatusPageState(): Promise { }) try { - const response = await fetch(STATUS_PAGE_WIDGET_URL, { + const response = await fetch(STATUS_PAGE_SUMMARY_URL, { cache: 'force-cache', next: { revalidate: STATUS_PAGE_CACHE_SECONDS }, signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS), @@ -85,8 +85,8 @@ async function getStatusPageState(): Promise { return 'unknown' } - const data = (await response.json()) as IncidentIOWidgetResponse - return getStatusPageStateFromWidget(data) + const data = (await response.json()) as IncidentIOStatusPageSummaryResponse + return getStatusPageStateFromSummary(data) } catch { return 'unknown' } diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts index bbc9b9f7e..4c5497dd7 100644 --- a/src/features/dashboard/layouts/status-indicator.ts +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -5,47 +5,53 @@ export type AggregateState = | 'maintenance' | 'unknown' -export interface IncidentIOWidgetEvent { - affected_components?: Array<{ +export interface IncidentIOStatusPageSummaryResponse { + status?: { + indicator?: string + } + components?: Array<{ + status?: string + }> + scheduled_maintenances?: Array<{ status?: string }> -} - -export interface IncidentIOWidgetResponse { - ongoing_incidents?: IncidentIOWidgetEvent[] - in_progress_maintenances?: IncidentIOWidgetEvent[] - scheduled_maintenances?: IncidentIOWidgetEvent[] } export function getStatusPageUrl() { - return (process.env.NEXT_PUBLIC_STATUS_PAGE_URL ?? 'https://status.e2b.dev') + return ( + process.env.NEXT_PUBLIC_STATUS_PAGE_URL ?? + 'https://statuspage.incident.io/e2b-service' + ) .trim() .replace(/\/+$/, '') } -export function getStatusPageWidgetUrl(statusPageUrl: string) { - const configuredWidgetUrl = - process.env.NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL?.trim() +export function getStatusPageSummaryUrl(statusPageUrl: string) { + const configuredSummaryUrl = + process.env.NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL?.trim() - if (configuredWidgetUrl) return configuredWidgetUrl + if (configuredSummaryUrl) return configuredSummaryUrl - return `${statusPageUrl}/api/widget` + return `${statusPageUrl}/api/v2/summary.json` } -function hasEvents(events: IncidentIOWidgetEvent[] | undefined) { - return Array.isArray(events) && events.length > 0 +function stateFromIndicator(indicator: string | undefined) { + if (indicator === 'none') return 'operational' + if (indicator === 'minor') return 'degraded' + if (indicator === 'major') return 'degraded' + if (indicator === 'critical') return 'downtime' + if (indicator === 'maintenance') return 'maintenance' + + return undefined } function getWorstComponentState( - events: IncidentIOWidgetEvent[] | undefined + components: IncidentIOStatusPageSummaryResponse['components'] ): AggregateState | undefined { const componentStatuses = - events?.flatMap( - (event) => - event.affected_components?.map((component) => component.status) ?? [] - ) ?? [] + components?.map((component) => component.status) ?? [] - if (componentStatuses.includes('full_outage')) return 'downtime' + if (componentStatuses.includes('major_outage')) return 'downtime' if (componentStatuses.includes('partial_outage')) return 'degraded' if (componentStatuses.includes('degraded_performance')) return 'degraded' if (componentStatuses.includes('under_maintenance')) return 'maintenance' @@ -53,14 +59,23 @@ function getWorstComponentState( return undefined } -export function getStatusPageStateFromWidget( - data: IncidentIOWidgetResponse -): AggregateState { - if (hasEvents(data.ongoing_incidents)) { - return getWorstComponentState(data.ongoing_incidents) ?? 'degraded' - } +function hasMaintenanceInProgress( + maintenances: IncidentIOStatusPageSummaryResponse['scheduled_maintenances'] +) { + return maintenances?.some( + (maintenance) => maintenance.status === 'maintenance_in_progress' + ) +} - if (hasEvents(data.in_progress_maintenances)) return 'maintenance' +export function getStatusPageStateFromSummary( + data: IncidentIOStatusPageSummaryResponse +): AggregateState { + if (hasMaintenanceInProgress(data.scheduled_maintenances)) + return 'maintenance' - return 'operational' + return ( + stateFromIndicator(data.status?.indicator) ?? + getWorstComponentState(data.components) ?? + 'unknown' + ) } diff --git a/src/lib/env.ts b/src/lib/env.ts index 0dad5ad00..80181e7c8 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -56,7 +56,7 @@ export const clientSchema = z.object({ NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(), NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR: z.string().optional(), NEXT_PUBLIC_STATUS_PAGE_URL: z.url().optional(), - NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL: z.url().optional(), + NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL: z.url().optional(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(), NEXT_PUBLIC_SCAN: z.string().optional(), NEXT_PUBLIC_MOCK_DATA: z.string().optional(), diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts index 3a247b77a..07f046bcd 100644 --- a/tests/unit/status-indicator.test.ts +++ b/tests/unit/status-indicator.test.ts @@ -1,61 +1,55 @@ import { describe, expect, it } from 'vitest' -import { getStatusPageStateFromWidget } from '@/features/dashboard/layouts/status-indicator' +import { getStatusPageStateFromSummary } from '@/features/dashboard/layouts/status-indicator' describe('status-indicator', () => { - it('should report operational when widget has no active events', () => { + it('should report operational for summary indicator none', () => { expect( - getStatusPageStateFromWidget({ - ongoing_incidents: [], - in_progress_maintenances: [], - scheduled_maintenances: [ - { - affected_components: [{ status: 'under_maintenance' }], - }, - ], + getStatusPageStateFromSummary({ + status: { + indicator: 'none', + }, }) ).toBe('operational') }) - it('should report maintenance for in-progress maintenances', () => { + it('should report maintenance for in-progress maintenance', () => { expect( - getStatusPageStateFromWidget({ - ongoing_incidents: [], - in_progress_maintenances: [{}], + getStatusPageStateFromSummary({ + scheduled_maintenances: [ + { + status: 'maintenance_in_progress', + }, + ], }) ).toBe('maintenance') }) - it('should report downtime for full outage incidents', () => { + it('should report downtime for critical summary indicator', () => { expect( - getStatusPageStateFromWidget({ - ongoing_incidents: [ - { - affected_components: [ - { status: 'degraded_performance' }, - { status: 'full_outage' }, - ], - }, - ], + getStatusPageStateFromSummary({ + status: { + indicator: 'critical', + }, }) ).toBe('downtime') }) - it('should report degraded for partial outage incidents', () => { + it('should report degraded for major summary indicator', () => { expect( - getStatusPageStateFromWidget({ - ongoing_incidents: [ - { - affected_components: [{ status: 'partial_outage' }], - }, - ], + getStatusPageStateFromSummary({ + status: { + indicator: 'major', + }, }) ).toBe('degraded') }) - it('should report degraded when incident has no component status', () => { + it('should report degraded for minor summary indicator', () => { expect( - getStatusPageStateFromWidget({ - ongoing_incidents: [{}], + getStatusPageStateFromSummary({ + status: { + indicator: 'minor', + }, }) ).toBe('degraded') }) From 134048c95f43bf0ea7e55e38994257fad63f1d83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:11:26 +0000 Subject: [PATCH 3/9] derive status api url Co-authored-by: Jakub Dobry --- .env.example | 1 - src/features/dashboard/layouts/status-indicator.ts | 5 ----- src/lib/env.ts | 1 - 3 files changed, 7 deletions(-) diff --git a/.env.example b/.env.example index 7069993d9..55ba4c808 100644 --- a/.env.example +++ b/.env.example @@ -71,7 +71,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key ### When enabled, the E2B status is read from the incident.io summary API # NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR=0 # NEXT_PUBLIC_STATUS_PAGE_URL=https://statuspage.incident.io/e2b-service -# NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL=https://statuspage.incident.io/e2b-service/api/v2/summary.json ### Set to 1 to use mock data # NEXT_PUBLIC_MOCK_DATA=0 diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts index 4c5497dd7..31df6f501 100644 --- a/src/features/dashboard/layouts/status-indicator.ts +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -27,11 +27,6 @@ export function getStatusPageUrl() { } export function getStatusPageSummaryUrl(statusPageUrl: string) { - const configuredSummaryUrl = - process.env.NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL?.trim() - - if (configuredSummaryUrl) return configuredSummaryUrl - return `${statusPageUrl}/api/v2/summary.json` } diff --git a/src/lib/env.ts b/src/lib/env.ts index 80181e7c8..36576effa 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -56,7 +56,6 @@ export const clientSchema = z.object({ NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(), NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR: z.string().optional(), NEXT_PUBLIC_STATUS_PAGE_URL: z.url().optional(), - NEXT_PUBLIC_STATUS_PAGE_SUMMARY_URL: z.url().optional(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(), NEXT_PUBLIC_SCAN: z.string().optional(), NEXT_PUBLIC_MOCK_DATA: z.string().optional(), From 60bcfee57a9670902b3b85ed561487607f73e19e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:18:44 +0000 Subject: [PATCH 4/9] keep old status link Co-authored-by: Jakub Dobry --- .env.example | 1 - .../dashboard/layouts/status-indicator.server.tsx | 8 +++----- .../dashboard/layouts/status-indicator.ts | 15 +++------------ src/lib/env.ts | 1 - 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index 55ba4c808..433bc4109 100644 --- a/.env.example +++ b/.env.example @@ -70,7 +70,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key ### Enable dashboard status indicator feature: set to 1 to enable ### When enabled, the E2B status is read from the incident.io summary API # NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR=0 -# NEXT_PUBLIC_STATUS_PAGE_URL=https://statuspage.incident.io/e2b-service ### Set to 1 to use mock data # NEXT_PUBLIC_MOCK_DATA=0 diff --git a/src/features/dashboard/layouts/status-indicator.server.tsx b/src/features/dashboard/layouts/status-indicator.server.tsx index 912e47f0d..17987d843 100644 --- a/src/features/dashboard/layouts/status-indicator.server.tsx +++ b/src/features/dashboard/layouts/status-indicator.server.tsx @@ -7,13 +7,11 @@ import { LiveDot } from '@/ui/live' import { type AggregateState, getStatusPageStateFromSummary, - getStatusPageSummaryUrl, - getStatusPageUrl, type IncidentIOStatusPageSummaryResponse, + STATUS_PAGE_LINK_URL, + STATUS_PAGE_SUMMARY_URL, } from './status-indicator' -export const STATUS_PAGE_URL = getStatusPageUrl() -const STATUS_PAGE_SUMMARY_URL = getStatusPageSummaryUrl(STATUS_PAGE_URL) const STATUS_PAGE_FETCH_TIMEOUT_MS = 5_000 const STATUS_PAGE_CACHE_SECONDS = 300 @@ -98,7 +96,7 @@ export default async function DashboardStatusBadgeServer() { return ( } -export function getStatusPageUrl() { - return ( - process.env.NEXT_PUBLIC_STATUS_PAGE_URL ?? - 'https://statuspage.incident.io/e2b-service' - ) - .trim() - .replace(/\/+$/, '') -} - -export function getStatusPageSummaryUrl(statusPageUrl: string) { - return `${statusPageUrl}/api/v2/summary.json` -} +export const STATUS_PAGE_LINK_URL = 'https://status.e2b.dev' +const INCIDENT_IO_STATUS_PAGE_URL = 'https://statuspage.incident.io/e2b-service' +export const STATUS_PAGE_SUMMARY_URL = `${INCIDENT_IO_STATUS_PAGE_URL}/api/v2/summary.json` function stateFromIndicator(indicator: string | undefined) { if (indicator === 'none') return 'operational' diff --git a/src/lib/env.ts b/src/lib/env.ts index 36576effa..75f054c65 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -55,7 +55,6 @@ export const clientSchema = z.object({ NEXT_PUBLIC_INCLUDE_ARGUS: z.string().optional(), NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(), NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR: z.string().optional(), - NEXT_PUBLIC_STATUS_PAGE_URL: z.url().optional(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(), NEXT_PUBLIC_SCAN: z.string().optional(), NEXT_PUBLIC_MOCK_DATA: z.string().optional(), From 717c042c9b3a5d0a16e171600daf9b434f40afba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:32:11 +0000 Subject: [PATCH 5/9] fix status priority Co-authored-by: Jakub Dobry --- .../dashboard/layouts/status-indicator.ts | 27 ++++++++++++++----- tests/unit/status-indicator.test.ts | 27 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts index 4a57758ff..8e7240246 100644 --- a/src/features/dashboard/layouts/status-indicator.ts +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -49,19 +49,32 @@ function hasMaintenanceInProgress( maintenances: IncidentIOStatusPageSummaryResponse['scheduled_maintenances'] ) { return maintenances?.some( - (maintenance) => maintenance.status === 'maintenance_in_progress' + (maintenance) => + maintenance.status === 'in_progress' || + maintenance.status === 'maintenance_in_progress' ) } export function getStatusPageStateFromSummary( data: IncidentIOStatusPageSummaryResponse ): AggregateState { - if (hasMaintenanceInProgress(data.scheduled_maintenances)) - return 'maintenance' + const indicatorState = stateFromIndicator(data.status?.indicator) + const componentState = getWorstComponentState(data.components) + + if (indicatorState === 'downtime' || componentState === 'downtime') + return 'downtime' + + if (indicatorState === 'degraded' || componentState === 'degraded') + return 'degraded' - return ( - stateFromIndicator(data.status?.indicator) ?? - getWorstComponentState(data.components) ?? - 'unknown' + if ( + indicatorState === 'maintenance' || + componentState === 'maintenance' || + hasMaintenanceInProgress(data.scheduled_maintenances) ) + return 'maintenance' + + if (indicatorState === 'operational') return 'operational' + + return 'unknown' } diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts index 07f046bcd..d542713f0 100644 --- a/tests/unit/status-indicator.test.ts +++ b/tests/unit/status-indicator.test.ts @@ -13,6 +13,18 @@ describe('status-indicator', () => { }) it('should report maintenance for in-progress maintenance', () => { + expect( + getStatusPageStateFromSummary({ + scheduled_maintenances: [ + { + status: 'in_progress', + }, + ], + }) + ).toBe('maintenance') + }) + + it('should support incident.io maintenance status naming', () => { expect( getStatusPageStateFromSummary({ scheduled_maintenances: [ @@ -24,6 +36,21 @@ describe('status-indicator', () => { ).toBe('maintenance') }) + it('should prioritize critical indicator over maintenance', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'critical', + }, + scheduled_maintenances: [ + { + status: 'in_progress', + }, + ], + }) + ).toBe('downtime') + }) + it('should report downtime for critical summary indicator', () => { expect( getStatusPageStateFromSummary({ From 32846c4db4110ab7df530aaab51cbf5907a0c9e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:47:39 +0000 Subject: [PATCH 6/9] cover component status Co-authored-by: Jakub Dobry --- .../dashboard/layouts/status-indicator.ts | 12 +++-- tests/unit/status-indicator.test.ts | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts index 8e7240246..280aa5edd 100644 --- a/src/features/dashboard/layouts/status-indicator.ts +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -47,11 +47,13 @@ function getWorstComponentState( function hasMaintenanceInProgress( maintenances: IncidentIOStatusPageSummaryResponse['scheduled_maintenances'] -) { - return maintenances?.some( - (maintenance) => - maintenance.status === 'in_progress' || - maintenance.status === 'maintenance_in_progress' +): boolean { + return ( + maintenances?.some( + (maintenance) => + maintenance.status === 'in_progress' || + maintenance.status === 'maintenance_in_progress' + ) ?? false ) } diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts index d542713f0..6ff7291b2 100644 --- a/tests/unit/status-indicator.test.ts +++ b/tests/unit/status-indicator.test.ts @@ -51,6 +51,54 @@ describe('status-indicator', () => { ).toBe('downtime') }) + it('should report downtime for major outage components', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'none', + }, + components: [ + { + status: 'degraded_performance', + }, + { + status: 'major_outage', + }, + ], + }) + ).toBe('downtime') + }) + + it('should report degraded for partial outage components', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'none', + }, + components: [ + { + status: 'partial_outage', + }, + ], + }) + ).toBe('degraded') + }) + + it('should prioritize degraded components over maintenance indicator', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'maintenance', + }, + components: [ + { + status: 'degraded_performance', + }, + ], + }) + ).toBe('degraded') + }) + it('should report downtime for critical summary indicator', () => { expect( getStatusPageStateFromSummary({ From d00e51135a0e70829c0d56732a33e2798be150de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:49:05 +0000 Subject: [PATCH 7/9] test maintenance component Co-authored-by: Jakub Dobry --- tests/unit/status-indicator.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts index 6ff7291b2..df4b0c65f 100644 --- a/tests/unit/status-indicator.test.ts +++ b/tests/unit/status-indicator.test.ts @@ -84,6 +84,21 @@ describe('status-indicator', () => { ).toBe('degraded') }) + it('should report maintenance for under maintenance components', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'none', + }, + components: [ + { + status: 'under_maintenance', + }, + ], + }) + ).toBe('maintenance') + }) + it('should prioritize degraded components over maintenance indicator', () => { expect( getStatusPageStateFromSummary({ From 18f969501cb23db16e280ef831c211f19af46c4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 20:07:11 +0000 Subject: [PATCH 8/9] lower status cache Co-authored-by: Jakub Dobry --- src/features/dashboard/layouts/status-indicator.server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/layouts/status-indicator.server.tsx b/src/features/dashboard/layouts/status-indicator.server.tsx index 17987d843..11983c048 100644 --- a/src/features/dashboard/layouts/status-indicator.server.tsx +++ b/src/features/dashboard/layouts/status-indicator.server.tsx @@ -13,7 +13,7 @@ import { } from './status-indicator' const STATUS_PAGE_FETCH_TIMEOUT_MS = 5_000 -const STATUS_PAGE_CACHE_SECONDS = 300 +const STATUS_PAGE_CACHE_SECONDS = 30 interface StatusUI { label: string From 5c64c4d126f2374af5308353beb507b73511a67f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 20:16:13 +0000 Subject: [PATCH 9/9] harden status mapping Co-authored-by: Jakub Dobry --- .../dashboard/layouts/status-indicator.ts | 93 ++++++++++++------- tests/unit/status-indicator.test.ts | 17 +++- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/features/dashboard/layouts/status-indicator.ts b/src/features/dashboard/layouts/status-indicator.ts index 280aa5edd..25a898ebd 100644 --- a/src/features/dashboard/layouts/status-indicator.ts +++ b/src/features/dashboard/layouts/status-indicator.ts @@ -21,28 +21,62 @@ export const STATUS_PAGE_LINK_URL = 'https://status.e2b.dev' const INCIDENT_IO_STATUS_PAGE_URL = 'https://statuspage.incident.io/e2b-service' export const STATUS_PAGE_SUMMARY_URL = `${INCIDENT_IO_STATUS_PAGE_URL}/api/v2/summary.json` -function stateFromIndicator(indicator: string | undefined) { - if (indicator === 'none') return 'operational' - if (indicator === 'minor') return 'degraded' - if (indicator === 'major') return 'degraded' - if (indicator === 'critical') return 'downtime' - if (indicator === 'maintenance') return 'maintenance' +const STATUS_PRIORITY: Record = { + unknown: 0, + operational: 1, + maintenance: 2, + degraded: 3, + downtime: 4, +} - return undefined +const INDICATOR_STATE: Record = { + none: 'operational', + minor: 'degraded', + major: 'degraded', + critical: 'downtime', + maintenance: 'maintenance', } -function getWorstComponentState( - components: IncidentIOStatusPageSummaryResponse['components'] +const COMPONENT_STATE: Record = { + operational: 'operational', + under_maintenance: 'maintenance', + degraded_performance: 'degraded', + partial_outage: 'degraded', + full_outage: 'downtime', + major_outage: 'downtime', +} + +const MAINTENANCE_IN_PROGRESS_STATUSES = new Set([ + 'in_progress', + 'maintenance_in_progress', +]) + +function stateFromValue( + value: string | undefined, + stateMap: Record +) { + return value ? stateMap[value] : undefined +} + +function highestPriorityState( + states: Array ): AggregateState | undefined { - const componentStatuses = - components?.map((component) => component.status) ?? [] + return states.reduce((highest, state) => { + if (!state) return highest + if (!highest) return state - if (componentStatuses.includes('major_outage')) return 'downtime' - if (componentStatuses.includes('partial_outage')) return 'degraded' - if (componentStatuses.includes('degraded_performance')) return 'degraded' - if (componentStatuses.includes('under_maintenance')) return 'maintenance' + return STATUS_PRIORITY[state] > STATUS_PRIORITY[highest] ? state : highest + }, undefined) +} - return undefined +function getWorstComponentState( + components: IncidentIOStatusPageSummaryResponse['components'] +): AggregateState | undefined { + return highestPriorityState( + components?.map((component) => + stateFromValue(component.status, COMPONENT_STATE) + ) ?? [] + ) } function hasMaintenanceInProgress( @@ -51,8 +85,8 @@ function hasMaintenanceInProgress( return ( maintenances?.some( (maintenance) => - maintenance.status === 'in_progress' || - maintenance.status === 'maintenance_in_progress' + !!maintenance.status && + MAINTENANCE_IN_PROGRESS_STATUSES.has(maintenance.status) ) ?? false ) } @@ -60,23 +94,14 @@ function hasMaintenanceInProgress( export function getStatusPageStateFromSummary( data: IncidentIOStatusPageSummaryResponse ): AggregateState { - const indicatorState = stateFromIndicator(data.status?.indicator) + const indicatorState = stateFromValue(data.status?.indicator, INDICATOR_STATE) const componentState = getWorstComponentState(data.components) + const maintenanceState = hasMaintenanceInProgress(data.scheduled_maintenances) + ? 'maintenance' + : undefined - if (indicatorState === 'downtime' || componentState === 'downtime') - return 'downtime' - - if (indicatorState === 'degraded' || componentState === 'degraded') - return 'degraded' - - if ( - indicatorState === 'maintenance' || - componentState === 'maintenance' || - hasMaintenanceInProgress(data.scheduled_maintenances) + return ( + highestPriorityState([indicatorState, componentState, maintenanceState]) ?? + 'unknown' ) - return 'maintenance' - - if (indicatorState === 'operational') return 'operational' - - return 'unknown' } diff --git a/tests/unit/status-indicator.test.ts b/tests/unit/status-indicator.test.ts index df4b0c65f..01d3871fe 100644 --- a/tests/unit/status-indicator.test.ts +++ b/tests/unit/status-indicator.test.ts @@ -51,7 +51,7 @@ describe('status-indicator', () => { ).toBe('downtime') }) - it('should report downtime for major outage components', () => { + it('should report downtime for full outage components', () => { expect( getStatusPageStateFromSummary({ status: { @@ -61,6 +61,21 @@ describe('status-indicator', () => { { status: 'degraded_performance', }, + { + status: 'full_outage', + }, + ], + }) + ).toBe('downtime') + }) + + it('should support major outage as a Statuspage compatibility alias', () => { + expect( + getStatusPageStateFromSummary({ + status: { + indicator: 'none', + }, + components: [ { status: 'major_outage', },