Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vercel/og": "^0.11.1",
"@vitejs/plugin-react": "^6.0.1",
"github-slugger": "^2.0.0",
Expand Down
30 changes: 15 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const Careers = lazy(() => import('./pages/Careers'));
const About = lazy(() => import('./pages/About'));
const Vitals = lazy(() => import('./pages/Vitals'));
const Security = lazy(() => import('./pages/Security'));
const Status = lazy(() => import('./pages/Status'));
const NotFound = lazy(() => import('./pages/NotFound'));
const Contributors = lazy(() => import('./pages/Contributors'));
const Blog = lazy(() => import('./pages/Blog'));
Expand Down Expand Up @@ -95,6 +96,15 @@ export default function App() {
</Layout>
}
/>
{/* Status page route */}
<Route
path="/status"
element={
<Layout>
<Status />
</Layout>
}
/>
{/* Wrap Stellar with Layout */}
<Route
path="/stellar"
Expand Down
40 changes: 40 additions & 0 deletions src/data/incidents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"incidents": [
{
"id": "inc-01",
"title": "RPC Latency Spike on Ethereum Mainnet",
"status": "resolved",
"impact": "minor",
"date": "2026-08-14T14:22:00Z",
"resolvedAt": "2026-08-14T15:10:00Z",
"updates": [
{
"timestamp": "2026-08-14T15:10:00Z",
"message": "The issue has been fully resolved following infrastructure scaling."
},
{
"timestamp": "2026-08-14T14:22:00Z",
"message": "We are investigating elevated latency across Ethereum RPC endpoints."
}
]
},
{
"id": "inc-02",
"title": "Scheduled Scanner Maintenance",
"status": "resolved",
"impact": "none",
"date": "2026-08-01T02:00:00Z",
"resolvedAt": "2026-08-01T03:30:00Z",
"updates": [
{
"timestamp": "2026-08-01T03:30:00Z",
"message": "Maintenance completed successfully with zero packet loss."
},
{
"timestamp": "2026-08-01T02:00:00Z",
"message": "Scheduled maintenance on the indexing scanner has begun."
}
]
}
]
}
105 changes: 105 additions & 0 deletions src/hooks/useStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useState, useEffect, useCallback } from 'react';
import incidentsData from '../data/incidents.json';

const statusApiUrl = import.meta.env.VITE_STATUS_API_URL || '';

export type ComponentStatus = {
id: string;
name: string;
status: 'operational' | 'degraded' | 'outage';
uptime90Days: number[]; // 90 days array of percentages or status codes (1 = up, 0.5 = degraded, 0 = down)
latencyMs: number;
};

export type Incident = {
id: string;
title: string;
status: 'investigating' | 'identified' | 'monitoring' | 'resolved';
impact: 'none' | 'minor' | 'major' | 'critical';
date: string;
resolvedAt?: string;
updates: { timestamp: string; message: string }[];
};

export type StatusData = {
overall: 'operational' | 'degraded' | 'outage';
components: ComponentStatus[];
incidents: Incident[];
lastUpdated: string;
};

export function useStatus() {
const [data, setData] = useState<StatusData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<string | null>(null);

const fetchStatus = useCallback(async () => {
if (!statusApiUrl) {
setData(null);
setError('Status endpoint unavailable');
setLoading(false);
setLastUpdated(null);
return;
}

try {
const headers: HeadersInit = {
Accept: 'application/json',
};
// Status polling sends no telemetry and never includes cookies.
const res = await fetch(statusApiUrl, { headers, credentials: 'omit' });
if (!res.ok) throw new Error(`Status endpoint returned ${res.status}`);
const json = await res.json();
if (!Array.isArray(json.components)) throw new Error('Status response has no components');
const apiComponents = json.components as ComponentStatus[];
if (!apiComponents.some((component) => /stellar/i.test(component.name))) {
throw new Error('Status response is missing Stellar');
}

setData({
overall: apiComponents.some((c) => c.status === 'outage')
? 'outage'
: apiComponents.some((c) => c.status === 'degraded')
? 'degraded'
: 'operational',
components: apiComponents,
incidents: incidentsData.incidents as Incident[],
lastUpdated: new Date().toISOString(),
});
setError(null);
setLastUpdated(new Date().toISOString());
} catch (err) {
setData(null);
setError(err instanceof Error ? err.message : 'Failed to fetch status data');
setLastUpdated(null);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchStatus();

// Auto-refresh every 60s, pause when tab is hidden
const interval = setInterval(() => {
if (document.visibilityState === 'visible') {
fetchStatus();
}
}, 60000);

const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
fetchStatus();
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
clearInterval(interval);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [fetchStatus]);

return { data, loading, error, refetch: fetchStatus, lastUpdated };
}
Loading
Loading