diff --git a/package.json b/package.json index 0c17f4d..de5c20c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71ecdf0..7a76f71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,16 +62,16 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@types/react': - specifier: ^19.2.14 - version: 19.2.14 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) + specifier: ^19.2.5 + version: 19.2.5(@types/react@19.2.18) '@vercel/og': specifier: ^0.11.1 version: 0.11.1 @@ -1201,13 +1201,13 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3604,15 +3604,15 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -3658,11 +3658,11 @@ snapshots: dependencies: undici-types: 7.19.2 - '@types/react-dom@19.2.3(@types/react@19.2.14)': + '@types/react-dom@19.2.5(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.18 - '@types/react@19.2.14': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 diff --git a/src/App.tsx b/src/App.tsx index 706d664..2464b54 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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')); @@ -95,6 +96,15 @@ export default function App() { } /> + {/* Status page route */} + + + + } + /> {/* Wrap Stellar with Layout */} (null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(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 }; +} diff --git a/src/pages/Status.tsx b/src/pages/Status.tsx new file mode 100644 index 0000000..3ac3cd9 --- /dev/null +++ b/src/pages/Status.tsx @@ -0,0 +1,165 @@ +import { useStatus } from '../hooks/useStatus'; + +export default function Status() { + const { data, loading, error, refetch, lastUpdated } = useStatus(); + + const getStatusBadge = (status: 'operational' | 'degraded' | 'outage') => { + switch (status) { + case 'operational': + return ( + + Operational + + ); + case 'degraded': + return ( + + Degraded Performance + + ); + case 'outage': + return ( + + Partial Outage + + ); + } + }; + + return ( +
+ {/* Header */} +
+
+

System Status

+

+ Real-time availability and uptime telemetry across Wraith Protocol infrastructure. +

+
+
+ {lastUpdated && ( + + Updated {new Date(lastUpdated).toLocaleTimeString()} + + )} + +
+
+ + {/* Overall Banner */} + {error && !data ? ( +
+

Status unavailable.

+

+ Live component data could not be loaded. Please try again later. +

+
+ ) : data ? ( +
+
+ {data.overall === 'operational' ? ( +
+ {getStatusBadge(data.overall)} +
+ ) : null} + + {/* Component Tiles */} +
+

Components

+ {data?.components.map((comp) => ( +
+
+
+

{comp.name}

+ Latency: {comp.latencyMs}ms +
+ {getStatusBadge(comp.status)} +
+ + {/* 90-Day Uptime Bar Chart */} +
+
+ 90 days ago + Today +
+
+ {comp.uptime90Days.map((val, idx) => ( +
0 ? 'bg-amber-500' : 'bg-red-500' + }`} + /> + ))} +
+
+
+ ))} +
+ + {/* Incident Log */} +
+

Incident History

+ {data?.incidents && data.incidents.length > 0 ? ( +
+ {data.incidents.map((incident) => ( +
+
+

{incident.title}

+ + {incident.status} + +
+

+ Started on {new Date(incident.date).toUTCString()} +

+
+ {incident.updates.map((update, idx) => ( +
+ + {new Date(update.timestamp).toUTCString()} + +

{update.message}

+
+ ))} +
+
+ ))} +
+ ) : ( +

+ No incidents reported in the recent period. +

+ )} +
+
+ ); +}