diff --git a/packages/frontend/app/components/Notifications/Notification.tsx b/packages/frontend/app/components/Notifications/Notification.tsx index 66131cca0..00842aab2 100644 --- a/packages/frontend/app/components/Notifications/Notification.tsx +++ b/packages/frontend/app/components/Notifications/Notification.tsx @@ -9,6 +9,7 @@ import { useAppSettings } from '~/stores/AppSettingsStore'; import type { notificationIF } from '~/stores/NotificationStore'; import styles from './Notification.module.css'; import { t } from 'i18next'; +import { FaDev } from 'react-icons/fa6'; interface propsIF { data: notificationIF; @@ -198,6 +199,9 @@ export default function Notification(props: propsIF) { color='var(--red)' /> )} + {data.icon === 'console' && ( + + )}

{ + const { default: Component } = await import( + './routes/status/Status' + ); + return { Component }; + }, + }, { path: 'v2/terms', lazy: async () => { diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css new file mode 100644 index 000000000..2c620265d --- /dev/null +++ b/packages/frontend/app/routes/status/Status.module.css @@ -0,0 +1,145 @@ +.container { + padding: var(--padding-l); + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--margin-l); +} + +.title { + font-size: var(--font-size-l); + color: var(--text1); + margin: 0; +} + +.refresh { + background: var(--accent1); + color: var(--text1); + border: none; + padding: var(--padding-s) var(--padding-m); + border-radius: var(--radius-xs); + font-size: var(--font-size-s); + cursor: pointer; + transition: all 0.2s ease; +} + +.refresh:hover { + background: var(--accent1-dark); +} + +.report_section { + background: var(--bg-dark2); + border: 1px solid var(--bg-dark4); + border-radius: var(--radius-s); + padding: var(--padding-m); + margin-bottom: var(--margin-l); +} + +.report_title { + font-size: var(--font-size-m); + color: var(--text1); + margin: 0 0 var(--margin-m) 0; +} + +.reports_list { + display: flex; + flex-direction: column; + gap: var(--gap-s); +} + +.report_item { + background: var(--bg-dark3); + border: 1px solid var(--bg-dark5); + border-radius: var(--radius-xs); + padding: var(--padding-m); +} + +.report_message { + color: var(--text1); + font-size: var(--font-size-s); + margin-bottom: var(--margin-s); + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; +} + +.report_timestamp { + color: var(--text3); + font-size: var(--font-size-xs); + font-family: var(--font-family-mono); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--gap-m); +} + +.card { + background: var(--bg-dark2); + border: 1px solid var(--bg-dark4); + border-radius: var(--radius-s); + padding: var(--padding-m); + transition: all 0.3s ease; +} + +.card:hover { + background: var(--bg-dark3); +} + +.card_title { + font-size: var(--font-size-m); + color: var(--text1); + margin-bottom: var(--margin-s); +} + +.card_status { + font-size: var(--font-size-s); + font-weight: 600; + margin-bottom: var(--margin-xs); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.card_time { + font-size: var(--font-size-s); + color: var(--text2); + margin-bottom: var(--margin-s); + font-family: var(--font-family-mono); +} + +.card_url { + font-size: var(--font-size-xs); + color: var(--text3); + word-break: break-all; + font-family: var(--font-family-mono); +} + +.status_operational { + border-color: var(--green); +} + +.status_operational .card_status { + color: var(--green); +} + +.status_down { + border-color: var(--red); +} + +.status_down .card_status { + color: var(--red); +} + +.status_checking { + border-color: var(--accent4); +} + +.status_checking .card_status { + color: var(--accent4); +} diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx new file mode 100644 index 000000000..e46b8b693 --- /dev/null +++ b/packages/frontend/app/routes/status/Status.tsx @@ -0,0 +1,180 @@ +import { useEffect, useState, type ReactElement } from 'react'; +import styles from './Status.module.css'; +import { + POLLING_API_INFO_ENDPOINT, + MARKET_INFO_ENDPOINT, + RPC_ENDPOINT, + blockExplorer, +} from '~/utils/Constants'; + +type EndpointStatus = { + name: string; + url: string; + status: 'checking' | 'operational' | 'down'; + responseTime?: number; + lastChecked?: Date; +}; + +type StatusReport = { + id: string; + message: string; + timestamp: Date; +}; + +const ENDPOINTS = [ + { name: 'Polling API', url: POLLING_API_INFO_ENDPOINT }, + { name: 'Market API', url: MARKET_INFO_ENDPOINT }, + { name: 'RPC Endpoint', url: RPC_ENDPOINT }, + { name: 'Block Explorer', url: blockExplorer }, + { + name: 'Ember', + url: 'https://ember-leaderboard-v2.liquidity.tools/health', + }, +]; + +export default function Status(): ReactElement { + const [endpoints, setEndpoints] = useState( + ENDPOINTS.map((ep) => ({ ...ep, status: 'checking' as const })), + ); + const [statusReports, setStatusReports] = useState([ + { + id: '1', + message: + 'All systems operational. Successfully processed 1.2M transactions in the last 24 hours.', + timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago + }, + { + id: '2', + message: + 'Scheduled maintenance completed. API response times improved by 15%.', + timestamp: new Date(Date.now() - 8 * 60 * 60 * 1000), // 8 hours ago + }, + { + id: '3', + message: + 'Investigating intermittent connectivity issues with RPC endpoint. Updates to follow.', + timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000), // 1 day ago + }, + ]); + + const checkEndpoint = async ( + endpoint: EndpointStatus, + ): Promise => { + const startTime = performance.now(); + + try { + await fetch(endpoint.url, { + method: 'HEAD', + mode: 'no-cors', + }); + + const endTime = performance.now(); + const responseTime = Math.round(endTime - startTime); + + return { + ...endpoint, + status: 'operational', + responseTime, + lastChecked: new Date(), + }; + } catch (error) { + const endTime = performance.now(); + const responseTime = Math.round(endTime - startTime); + + return { + ...endpoint, + status: 'down', + responseTime, + lastChecked: new Date(), + }; + } + }; + + const checkAllEndpoints = async (): Promise => { + setEndpoints((prev) => + prev.map((ep) => ({ ...ep, status: 'checking' as const })), + ); + + const updatedEndpoints = await Promise.all( + endpoints.map(checkEndpoint), + ); + + setEndpoints(updatedEndpoints); + }; + + const fetchStatusReports = async (): Promise => { + // TODO: Replace with actual API call to fetch status reports + // const response = await fetch('/api/status-reports'); + // const data = await response.json(); + // setStatusReports(data); + }; + + useEffect(() => { + checkAllEndpoints(); + fetchStatusReports(); + }, []); + + const getStatusColor = (status: EndpointStatus['status']): string => { + switch (status) { + case 'operational': + return styles.status_operational; + case 'down': + return styles.status_down; + case 'checking': + return styles.status_checking; + } + }; + + return ( +
+
+

System Status

+ +
+ + {statusReports.length > 0 && ( +
+

Status Updates

+
+ {statusReports.map((report) => ( +
+
+ {report.message} +
+
+ {report.timestamp.toLocaleString()} +
+
+ ))} +
+
+ )} + +
+ {endpoints.map((endpoint) => ( +
+

{endpoint.name}

+
+ {endpoint.status === 'checking' + ? 'Checking...' + : endpoint.status === 'operational' + ? 'Operational' + : 'Down'} +
+ {endpoint.responseTime !== undefined && ( +
+ {endpoint.responseTime}ms +
+ )} +
{endpoint.url}
+
+ ))} +
+
+ ); +} diff --git a/packages/frontend/app/stores/NotificationStore.ts b/packages/frontend/app/stores/NotificationStore.ts index f4e4d1b61..ac17ac7cd 100644 --- a/packages/frontend/app/stores/NotificationStore.ts +++ b/packages/frontend/app/stores/NotificationStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; // slugs to indicate which React icon should be rendered -type icons = 'spinner' | 'check' | 'error'; +type icons = 'spinner' | 'check' | 'error' | 'console'; // shape of post-processed data used to construct a DOM element export interface notificationIF {