From 9145ae07a3af69fd06bdf11acacb96fd0a4a7d72 Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 13:33:08 -0500 Subject: [PATCH 01/11] create API status page --- packages/frontend/app/entry.client.tsx | 9 + .../app/routes/status/Status.module.css | 350 ++++++++++ .../frontend/app/routes/status/Status.tsx | 614 ++++++++++++++++++ 3 files changed, 973 insertions(+) create mode 100644 packages/frontend/app/routes/status/Status.module.css create mode 100644 packages/frontend/app/routes/status/Status.tsx diff --git a/packages/frontend/app/entry.client.tsx b/packages/frontend/app/entry.client.tsx index 61b1705b3..e38a864a3 100644 --- a/packages/frontend/app/entry.client.tsx +++ b/packages/frontend/app/entry.client.tsx @@ -250,6 +250,15 @@ const router = createBrowserRouter([ return { Component }; }, }, + { + path: 'v2/status', + lazy: async () => { + 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..e142737ff --- /dev/null +++ b/packages/frontend/app/routes/status/Status.module.css @@ -0,0 +1,350 @@ +.status_page { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.page_header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.title { + font-size: 2rem; + font-weight: bold; + margin: 0; + color: var(--text-primary, #ffffff); +} + +.endpoints_container { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.endpoint_card { + background: var(--bg-secondary, #1a1a1a); + border: 1px solid var(--border-color, #333); + border-radius: 8px; + padding: 1rem; + transition: box-shadow 0.2s ease; + display: flex; + flex-direction: column; +} + +.endpoint_card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.endpoint_header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color, #333); +} + +.endpoint_name { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary, #ffffff); + margin: 0; +} + +.header_actions { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.refresh_icon_button { + background: transparent; + border: 1px solid var(--border-color, #333); + color: var(--text-secondary, #999); + padding: 0.5rem; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.refresh_icon_button:hover:not(:disabled) { + background: var(--bg-tertiary, #2a2a2a); + color: var(--text-primary, #ffffff); + border-color: var(--accent1); +} + +.refresh_icon_button:active:not(:disabled) { + transform: scale(0.95); +} + +.refresh_icon_button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.refresh_icon_button svg { + width: 18px; + height: 18px; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.spinning { + animation: spin 1s linear infinite; +} + +.status_badge { + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status_operational { + background: rgba(34, 197, 94, 0.2); + color: #22c55e; + border: 1px solid #22c55e; +} + +.status_down { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; + border: 1px solid #ef4444; +} + +.status_checking { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; + border: 1px solid #3b82f6; +} + +.loading_container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + gap: 0.75rem; + padding: 1.5rem 0; + min-height: 100px; +} + +.loading_spinner { + width: 32px; + height: 32px; + color: var(--accent1); + animation: spin 1s linear infinite; +} + +.loading_text { + color: var(--text-secondary, #999); + font-size: 0.875rem; +} + +.endpoint_details { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; + min-height: 100px; +} + +.detail_row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.detail_label { + font-weight: 600; + color: var(--text-secondary, #999); + min-width: 130px; +} + +.detail_value { + color: var(--text-primary, #ffffff); + font-family: monospace; + font-size: 0.875rem; +} + +.error_text { + color: #ef4444; +} + +.refresh_button { + background: var(--accent-color, #3b82f6); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.refresh_button:hover { + background: var(--accent-color-hover, #2563eb); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); +} + +.refresh_button:active { + transform: translateY(0); +} + +.accordion_container { + margin-top: 0.75rem; + border-top: 1px solid var(--border-color, #333); + padding-top: 0.5rem; +} + +.accordion_buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.accordion_button { + width: 180px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.25rem; + background: transparent; + border: 1px solid var(--border-color, #333); + color: var(--text-secondary, #999); + padding: 0.5rem 0.75rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + border-radius: 6px; + transition: all 0.2s ease; +} + +.accordion_button_title { + font-size: 0.875rem; + font-weight: 500; +} + +.accordion_button_subtitle { + font-size: 0.75rem; + opacity: 0.7; +} + +.accordion_button_request:hover { + color: var(--text-primary, #ffffff); + border-color: #c586c0; + background: var(--bg-tertiary, #2a2a2a); +} + +.accordion_button_request_expanded { + color: var(--text-primary, #ffffff); + border-color: #c586c0; + background: var(--bg-tertiary, #2a2a2a); +} + +.accordion_button_response:hover { + color: var(--text-primary, #ffffff); + border-color: #4fc1ff; + background: var(--bg-tertiary, #2a2a2a); +} + +.accordion_button_response_expanded { + color: var(--text-primary, #ffffff); + border-color: #4fc1ff; + background: var(--bg-tertiary, #2a2a2a); +} + +.accordion_contents { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.accordion_item { + display: flex; + gap: 0.5rem; + align-items: stretch; +} + +.accordion_label_text { + font-size: 0.875rem; + font-weight: 600; + writing-mode: vertical-rl; + text-orientation: mixed; + transform: rotate(180deg); + flex-shrink: 0; + padding: 0.25rem 0; +} + +.accordion_label_request { + color: #c586c0; +} + +.accordion_label_response { + color: #4fc1ff; +} + +.accordion_content { + background: var(--bg-tertiary, #0a0a0a); + border: 1px solid var(--border-color, #333); + border-radius: 6px; + padding: 0.75rem; + flex: 1; + overflow-x: auto; +} + +.json_display { + margin: 0; + font-family: + 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', + monospace; + font-size: 0.813rem; + line-height: 1.5; + white-space: pre; + overflow-x: auto; +} + +/* JSON Syntax Highlighting - Standard color scheme */ +.json_key { + color: #4ec9b0; +} + +.json_string { + color: #ce9178; +} + +.json_number { + color: #b5cea8; +} + +.json_boolean { + color: #569cd6; +} + +.json_null { + color: #569cd6; +} + +.json_punctuation { + color: #d4d4d4; +} diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx new file mode 100644 index 000000000..884a2df37 --- /dev/null +++ b/packages/frontend/app/routes/status/Status.tsx @@ -0,0 +1,614 @@ +import { useEffect, useState, type ReactElement } from 'react'; +import { LuRefreshCcw } from 'react-icons/lu'; +import styles from './Status.module.css'; +import { POLLING_API_INFO_ENDPOINT, RPC_ENDPOINT } from '~/utils/Constants'; + +type EndpointStatus = { + name: string; + url: string; + status: 'checking' | 'operational' | 'down'; + responseTime?: number; + lastChecked?: Date; + error?: string; + requestDetails?: { + method: string; + headers: Record; + body: unknown; + }; + responseDetails?: { + status: number; + statusText: string; + headers: Record; + body: unknown; + }; +}; + +const MIN_SPIN_TIME = 0.5; // seconds + +// JSON syntax highlighter component +const JsonHighlight = ({ data }: { data: unknown }) => { + const highlightJson = (obj: unknown, indent = 0): ReactElement[] => { + const elements: ReactElement[] = []; + const indentStr = ' '.repeat(indent); + + if (obj === null) { + elements.push( + + null + , + ); + } else if (typeof obj === 'boolean') { + elements.push( + + {String(obj)} + , + ); + } else if (typeof obj === 'number') { + elements.push( + + {obj} + , + ); + } else if (typeof obj === 'string') { + elements.push( + + "{obj}" + , + ); + } else if (Array.isArray(obj)) { + elements.push( + + {'[\n'} + , + ); + obj.forEach((item, index) => { + elements.push( + + {indentStr}{' '} + , + ); + elements.push(...highlightJson(item, indent + 1)); + if (index < obj.length - 1) { + elements.push( + + , + , + ); + } + elements.push( + {'\n'}, + ); + }); + elements.push({indentStr}); + elements.push( + + ] + , + ); + } else if (typeof obj === 'object') { + elements.push( + + {'{\n'} + , + ); + const entries = Object.entries(obj); + entries.forEach(([key, value], index) => { + elements.push( + + {indentStr}{' '} + , + ); + elements.push( + + "{key}" + , + ); + elements.push( + + :{' '} + , + ); + elements.push(...highlightJson(value, indent + 1)); + if (index < entries.length - 1) { + elements.push( + + , + , + ); + } + elements.push( + {'\n'}, + ); + }); + elements.push({indentStr}); + elements.push( + + {'}'} + , + ); + } + + return elements; + }; + + return
{highlightJson(data)}
; +}; + +export default function Status() { + const [endpoints, setEndpoints] = useState([ + { + name: 'Polling API Info Endpoint', + url: POLLING_API_INFO_ENDPOINT, + status: 'checking', + }, + { + name: 'RPC Endpoint', + url: RPC_ENDPOINT, + status: 'checking', + }, + ]); + + const [expandedAccordions, setExpandedAccordions] = useState>( + new Set(), + ); + + const toggleAccordion = (key: string) => { + setExpandedAccordions((prev) => { + const newSet = new Set(prev); + if (newSet.has(key)) { + newSet.delete(key); + } else { + newSet.add(key); + } + return newSet; + }); + }; + + const checkEndpoint = async ( + endpoint: EndpointStatus, + ): Promise => { + const startTime = performance.now(); + + const requestBody = { type: 'meta' }; + const requestHeaders = { 'Content-Type': 'application/json' }; + const requestDetails = { + method: 'POST', + headers: requestHeaders, + body: requestBody, + }; + + try { + const response = await fetch(endpoint.url, { + method: 'POST', + headers: requestHeaders, + body: JSON.stringify(requestBody), + }); + + const endTime = performance.now(); + const responseTime = Math.round(endTime - startTime); + + // Extract response details + const responseBody = await response.json().catch(() => null); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + const responseDetails = { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + body: responseBody, + }; + + if (response.ok) { + return { + ...endpoint, + status: 'operational', + responseTime, + lastChecked: new Date(), + requestDetails, + responseDetails, + }; + } else { + return { + ...endpoint, + status: 'down', + responseTime, + lastChecked: new Date(), + error: `HTTP ${response.status}`, + requestDetails, + responseDetails, + }; + } + } catch (error) { + const endTime = performance.now(); + const responseTime = Math.round(endTime - startTime); + + return { + ...endpoint, + status: 'down', + responseTime, + lastChecked: new Date(), + error: error instanceof Error ? error.message : 'Unknown error', + requestDetails, + }; + } + }; + + const checkAllEndpoints = async () => { + const startTime = performance.now(); + + // Set all endpoints to checking first + setEndpoints((prev) => + prev.map((ep) => ({ ...ep, status: 'checking' as const })), + ); + + // Check all endpoints in parallel + const updatedEndpoints = await Promise.all( + endpoints.map(checkEndpoint), + ); + + // Ensure minimum spinner duration + const elapsedTime = performance.now() - startTime; + const remainingTime = Math.max(0, MIN_SPIN_TIME * 1000 - elapsedTime); + + if (remainingTime > 0) { + await new Promise((resolve) => setTimeout(resolve, remainingTime)); + } + + setEndpoints(updatedEndpoints); + }; + + const checkSingleEndpoint = async (index: number) => { + const endpoint = endpoints[index]; + const startTime = performance.now(); + + // Set status to checking first + setEndpoints((prev) => + prev.map((ep, i) => + i === index ? { ...ep, status: 'checking' as const } : ep, + ), + ); + + // Perform the check + const updatedEndpoint = await checkEndpoint(endpoint); + + // Ensure minimum spinner duration + const elapsedTime = performance.now() - startTime; + const remainingTime = Math.max(0, MIN_SPIN_TIME * 1000 - elapsedTime); + + if (remainingTime > 0) { + await new Promise((resolve) => setTimeout(resolve, remainingTime)); + } + + // Update with result + setEndpoints((prev) => + prev.map((ep, i) => (i === index ? updatedEndpoint : ep)), + ); + }; + + useEffect(() => { + checkAllEndpoints(); + + // Check every 30 seconds + const interval = setInterval(checkAllEndpoints, 30000); + + return () => clearInterval(interval); + }, []); + + const getStatusColor = (status: EndpointStatus['status']) => { + switch (status) { + case 'operational': + return styles.status_operational; + case 'down': + return styles.status_down; + case 'checking': + return styles.status_checking; + } + }; + + return ( +
+
+

System Status

+ +
+ +
+ {endpoints.map((endpoint, index) => ( +
+
+

+ {endpoint.name} +

+
+ + + {endpoint.status === 'checking' + ? 'Checking...' + : endpoint.status === 'operational' + ? 'Operational' + : 'Down'} + +
+
+ + {endpoint.status === 'checking' ? ( +
+ + + Checking endpoint... + +
+ ) : ( + <> +
+
+ + URL: + + + {endpoint.url} + +
+ + {endpoint.responseTime !== undefined && ( +
+ + Response Time: + + + {endpoint.responseTime}ms + +
+ )} + + {endpoint.lastChecked && ( +
+ + Last Checked: + + + {endpoint.lastChecked.toLocaleTimeString()} + +
+ )} + + {endpoint.error && ( +
+ + Error: + + + {endpoint.error} + +
+ )} +
+ + {(endpoint.requestDetails || + endpoint.responseDetails) && ( +
+
+ {endpoint.requestDetails && + (() => { + const jsonString = + JSON.stringify( + endpoint.requestDetails, + null, + 2, + ); + const lineCount = + jsonString.split( + '\n', + ).length; + return ( + + ); + })()} + {endpoint.responseDetails && + (() => { + const jsonString = + JSON.stringify( + endpoint.responseDetails, + null, + 2, + ); + const lineCount = + jsonString.split( + '\n', + ).length; + return ( + + ); + })()} +
+
+ {expandedAccordions.has( + `${index}-request`, + ) && + endpoint.requestDetails && ( +
+
+ Request Details +
+
+ +
+
+ )} + {expandedAccordions.has( + `${index}-response`, + ) && + endpoint.responseDetails && ( +
+
+ Response Details +
+
+ +
+
+ )} +
+
+ )} + + )} +
+ ))} +
+
+ ); +} From 5ad5cef221f99f33c1cdb43e6ae0e2abbab96f2d Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 13:43:41 -0500 Subject: [PATCH 02/11] add copy to clipboard functionality for endpoints --- .../app/routes/status/Status.module.css | 27 +++++++++++++++++ .../frontend/app/routes/status/Status.tsx | 30 ++++++++++++++++++- packages/frontend/scripts/.tv-copy-cache.json | 2 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index e142737ff..81cc9f908 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -171,6 +171,33 @@ gap: 0.75rem; } +.copy_button { + background: transparent; + border: none; + color: var(--text-secondary, #999); + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; + border-radius: 4px; +} + +.copy_button:hover { + color: var(--text-primary, #ffffff); + background: var(--bg-tertiary, #2a2a2a); +} + +.copy_button svg { + width: 16px; + height: 16px; +} + +.copy_icon_success { + color: #22c55e; +} + .detail_label { font-weight: 600; color: var(--text-secondary, #999); diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 884a2df37..86cebd8cd 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type ReactElement } from 'react'; -import { LuRefreshCcw } from 'react-icons/lu'; +import { LuRefreshCcw, LuCopy, LuCheck } from 'react-icons/lu'; import styles from './Status.module.css'; import { POLLING_API_INFO_ENDPOINT, RPC_ENDPOINT } from '~/utils/Constants'; @@ -175,6 +175,7 @@ export default function Status() { const [expandedAccordions, setExpandedAccordions] = useState>( new Set(), ); + const [copiedUrl, setCopiedUrl] = useState(null); const toggleAccordion = (key: string) => { setExpandedAccordions((prev) => { @@ -188,6 +189,16 @@ export default function Status() { }); }; + const copyToClipboard = async (url: string) => { + try { + await navigator.clipboard.writeText(url); + setCopiedUrl(url); + setTimeout(() => setCopiedUrl(null), 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + const checkEndpoint = async ( endpoint: EndpointStatus, ): Promise => { @@ -397,6 +408,23 @@ export default function Status() { {endpoint.url} + {endpoint.responseTime !== undefined && ( diff --git a/packages/frontend/scripts/.tv-copy-cache.json b/packages/frontend/scripts/.tv-copy-cache.json index 2c70ed585..c096b5eca 100644 --- a/packages/frontend/scripts/.tv-copy-cache.json +++ b/packages/frontend/scripts/.tv-copy-cache.json @@ -1,5 +1,5 @@ { - "../app/tv/charting_library": "ccec5eacb98b3103c2b535fa0b502d94", + "../app/tv/charting_library": "9e7882f78967fb896cfa7f501fb0f4b6", "../app/tv/datafeeds/udf/dist/bundle.js": "ed05ccf26f855d5a331469d932c2c8a6", "../app/css/tradingview-overrides.css": "b59dce03d6797e8560a9563e1b7f8dfc" } From 33fb32f2c954704a44c9500c4dd35ed9e6e5220a Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 13:53:49 -0500 Subject: [PATCH 03/11] add mobile responsiveness --- .../app/routes/status/Status.module.css | 154 ++++++++++++++++-- .../frontend/app/routes/status/Status.tsx | 8 +- 2 files changed, 146 insertions(+), 16 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index 81cc9f908..d3f350a16 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -276,10 +276,18 @@ opacity: 0.7; } -.accordion_button_request:hover { - color: var(--text-primary, #ffffff); - border-color: #c586c0; - background: var(--bg-tertiary, #2a2a2a); +@media (hover: hover) { + .accordion_button_request:hover { + color: var(--text-primary, #ffffff); + border-color: #c586c0; + background: var(--bg-tertiary, #2a2a2a); + } + + .accordion_button_response:hover { + color: var(--text-primary, #ffffff); + border-color: #4fc1ff; + background: var(--bg-tertiary, #2a2a2a); + } } .accordion_button_request_expanded { @@ -288,12 +296,6 @@ background: var(--bg-tertiary, #2a2a2a); } -.accordion_button_response:hover { - color: var(--text-primary, #ffffff); - border-color: #4fc1ff; - background: var(--bg-tertiary, #2a2a2a); -} - .accordion_button_response_expanded { color: var(--text-primary, #ffffff); border-color: #4fc1ff; @@ -340,6 +342,14 @@ overflow-x: auto; } +.accordion_content_request { + border-color: #c586c0; +} + +.accordion_content_response { + border-color: #4fc1ff; +} + .json_display { margin: 0; font-family: @@ -375,3 +385,127 @@ .json_punctuation { color: #d4d4d4; } + +/* Mobile Responsive Styles */ +@media (max-width: 768px) { + .status_page { + padding: 1rem; + } + + .page_header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.5rem; + } + + .title { + font-size: 1.5rem; + } + + .refresh_button { + width: 100%; + } + + .endpoint_card { + padding: 0.75rem; + } + + .endpoint_name { + font-size: 1.125rem; + } + + .accordion_buttons { + flex-direction: column; + width: 100%; + } + + .accordion_button { + width: 100%; + } + + .detail_row { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .detail_label { + min-width: auto; + } + + .copy_button { + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + + .detail_row:first-child { + position: relative; + padding-right: 2rem; + } +} + +@media (max-width: 480px) { + .status_page { + padding: 0.75rem; + } + + .title { + font-size: 1.25rem; + } + + .endpoint_card { + padding: 0.5rem; + border-radius: 6px; + } + + .endpoint_header { + flex-wrap: wrap; + gap: 0.5rem; + align-items: flex-start; + } + + .endpoint_name { + font-size: 1rem; + flex: 0 1 auto; + } + + .header_actions { + display: contents; + } + + .refresh_icon_button { + padding: 0.375rem; + flex: 0 0 auto; + } + + .status_badge { + font-size: 0.75rem; + padding: 0.375rem 0.75rem; + flex: 1 1 100%; + text-align: center; + } + + .detail_value { + font-size: 0.75rem; + word-break: break-all; + } + + .accordion_button_title { + font-size: 0.813rem; + } + + .accordion_button_subtitle { + font-size: 0.688rem; + } + + .accordion_label_text { + font-size: 0.75rem; + } + + .json_display { + font-size: 0.75rem; + line-height: 1.4; + } +} diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 86cebd8cd..312b56c7b 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -590,9 +590,7 @@ export default function Status() { Request Details
Date: Wed, 19 Nov 2025 13:58:27 -0500 Subject: [PATCH 04/11] remove auto-refresh --- packages/frontend/app/routes/status/Status.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 312b56c7b..372539ab7 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -325,11 +325,6 @@ export default function Status() { useEffect(() => { checkAllEndpoints(); - - // Check every 30 seconds - const interval = setInterval(checkAllEndpoints, 30000); - - return () => clearInterval(interval); }, []); const getStatusColor = (status: EndpointStatus['status']) => { From ecc0ae017a87fc9c0dee847ebda7ebafc1331f74 Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 14:30:29 -0500 Subject: [PATCH 05/11] add scrolling to large code blocks on status page --- .../app/routes/status/Status.module.css | 18 ++++--- .../frontend/app/routes/status/Status.tsx | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index d3f350a16..cdbacc45d 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -279,26 +279,26 @@ @media (hover: hover) { .accordion_button_request:hover { color: var(--text-primary, #ffffff); - border-color: #c586c0; + border-color: var(--accent2); background: var(--bg-tertiary, #2a2a2a); } .accordion_button_response:hover { color: var(--text-primary, #ffffff); - border-color: #4fc1ff; + border-color: var(--accent3); background: var(--bg-tertiary, #2a2a2a); } } .accordion_button_request_expanded { color: var(--text-primary, #ffffff); - border-color: #c586c0; + border-color: var(--accent2); background: var(--bg-tertiary, #2a2a2a); } .accordion_button_response_expanded { color: var(--text-primary, #ffffff); - border-color: #4fc1ff; + border-color: var(--accent3); background: var(--bg-tertiary, #2a2a2a); } @@ -326,11 +326,11 @@ } .accordion_label_request { - color: #c586c0; + color: var(--accent2); } .accordion_label_response { - color: #4fc1ff; + color: var(--accent3); } .accordion_content { @@ -339,15 +339,17 @@ border-radius: 6px; padding: 0.75rem; flex: 1; + max-height: 600px; + overflow-y: auto; overflow-x: auto; } .accordion_content_request { - border-color: #c586c0; + border-color: var(--accent2); } .accordion_content_response { - border-color: #4fc1ff; + border-color: var(--accent3); } .json_display { diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 372539ab7..31562c92a 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, type ReactElement } from 'react'; import { LuRefreshCcw, LuCopy, LuCheck } from 'react-icons/lu'; import styles from './Status.module.css'; import { POLLING_API_INFO_ENDPOINT, RPC_ENDPOINT } from '~/utils/Constants'; +import { useKeydown } from '~/hooks/useKeydown'; type EndpointStatus = { name: string; @@ -10,6 +11,8 @@ type EndpointStatus = { responseTime?: number; lastChecked?: Date; error?: string; + requestTimestamp?: number; + responseTimestamp?: number; requestDetails?: { method: string; headers: Record; @@ -177,6 +180,15 @@ export default function Status() { ); const [copiedUrl, setCopiedUrl] = useState(null); + // Close all accordions on Escape key + useKeydown( + 'Escape', + () => { + setExpandedAccordions(new Set()); + }, + [], + ); + const toggleAccordion = (key: string) => { setExpandedAccordions((prev) => { const newSet = new Set(prev); @@ -203,6 +215,7 @@ export default function Status() { endpoint: EndpointStatus, ): Promise => { const startTime = performance.now(); + const requestTimestamp = Date.now(); const requestBody = { type: 'meta' }; const requestHeaders = { 'Content-Type': 'application/json' }; @@ -220,6 +233,7 @@ export default function Status() { }); const endTime = performance.now(); + const responseTimestamp = Date.now(); const responseTime = Math.round(endTime - startTime); // Extract response details @@ -242,6 +256,8 @@ export default function Status() { status: 'operational', responseTime, lastChecked: new Date(), + requestTimestamp, + responseTimestamp, requestDetails, responseDetails, }; @@ -251,6 +267,8 @@ export default function Status() { status: 'down', responseTime, lastChecked: new Date(), + requestTimestamp, + responseTimestamp, error: `HTTP ${response.status}`, requestDetails, responseDetails, @@ -258,6 +276,7 @@ export default function Status() { } } catch (error) { const endTime = performance.now(); + const responseTimestamp = Date.now(); const responseTime = Math.round(endTime - startTime); return { @@ -265,6 +284,8 @@ export default function Status() { status: 'down', responseTime, lastChecked: new Date(), + requestTimestamp, + responseTimestamp, error: error instanceof Error ? error.message : 'Unknown error', requestDetails, }; @@ -437,6 +458,38 @@ export default function Status() {
)} + {endpoint.requestTimestamp !== + undefined && ( +
+ + Request Dispatched: + + + {endpoint.requestTimestamp} + +
+ )} + + {endpoint.responseTimestamp !== + undefined && ( +
+ + Response Received: + + + {endpoint.responseTimestamp} + +
+ )} + {endpoint.lastChecked && (
Date: Wed, 19 Nov 2025 14:42:11 -0500 Subject: [PATCH 06/11] add code to rate API response times --- .../app/routes/status/Status.module.css | 40 +++++ .../frontend/app/routes/status/Status.tsx | 139 ++++++++++++++++-- 2 files changed, 167 insertions(+), 12 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index cdbacc45d..1c70f7f70 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -214,6 +214,46 @@ color: #ef4444; } +.status_code_1xx { + color: #71b5fc; +} + +.status_code_2xx { + color: #26a69a; +} + +.status_code_3xx { + color: #e9e980; +} + +.status_code_4xx { + color: #e78a56; +} + +.status_code_5xx { + color: #ef5350; +} + +.response_time_great { + color: #26a69a; + font-weight: 600; +} + +.response_time_ok { + color: #e9e980; + font-weight: 600; +} + +.response_time_bad { + color: #e78a56; + font-weight: 600; +} + +.response_time_very_bad { + color: #ef5350; + font-weight: 600; +} + .refresh_button { background: var(--accent-color, #3b82f6); color: white; diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 31562c92a..beee1d1fb 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -4,6 +4,16 @@ import styles from './Status.module.css'; import { POLLING_API_INFO_ENDPOINT, RPC_ENDPOINT } from '~/utils/Constants'; import { useKeydown } from '~/hooks/useKeydown'; +interface EndpointConfig { + name: string; + url: string; + tolerances: { + great: number; + ok: number; + bad: number; + }; +} + type EndpointStatus = { name: string; url: string; @@ -13,6 +23,11 @@ type EndpointStatus = { error?: string; requestTimestamp?: number; responseTimestamp?: number; + tolerances: { + great: number; + ok: number; + bad: number; + }; requestDetails?: { method: string; headers: Record; @@ -26,6 +41,27 @@ type EndpointStatus = { }; }; +const ENDPOINTS_TO_MONITOR: EndpointConfig[] = [ + { + name: 'Polling API Info Endpoint', + url: POLLING_API_INFO_ENDPOINT, + tolerances: { + great: 100, + ok: 200, + bad: 300, + }, + }, + { + name: 'RPC Endpoint', + url: RPC_ENDPOINT, + tolerances: { + great: 100, + ok: 200, + bad: 300, + }, + }, +]; + const MIN_SPIN_TIME = 0.5; // seconds // JSON syntax highlighter component @@ -162,18 +198,12 @@ const JsonHighlight = ({ data }: { data: unknown }) => { }; export default function Status() { - const [endpoints, setEndpoints] = useState([ - { - name: 'Polling API Info Endpoint', - url: POLLING_API_INFO_ENDPOINT, - status: 'checking', - }, - { - name: 'RPC Endpoint', - url: RPC_ENDPOINT, - status: 'checking', - }, - ]); + const [endpoints, setEndpoints] = useState( + ENDPOINTS_TO_MONITOR.map((ep) => ({ + ...ep, + status: 'checking' as const, + })), + ); const [expandedAccordions, setExpandedAccordions] = useState>( new Set(), @@ -359,6 +389,36 @@ export default function Status() { } }; + const getStatusCodeColor = (statusCode: number) => { + if (statusCode >= 100 && statusCode < 200) + return styles.status_code_1xx; + if (statusCode >= 200 && statusCode < 300) + return styles.status_code_2xx; + if (statusCode >= 300 && statusCode < 400) + return styles.status_code_3xx; + if (statusCode >= 400 && statusCode < 500) + return styles.status_code_4xx; + if (statusCode >= 500 && statusCode < 600) + return styles.status_code_5xx; + return ''; + }; + + const getResponseTimeQuality = ( + responseTime: number, + tolerances: EndpointStatus['tolerances'], + ): { label: string; className: string } => { + if (responseTime <= tolerances.great) { + return { label: 'Great', className: styles.response_time_great }; + } + if (responseTime <= tolerances.ok) { + return { label: 'OK', className: styles.response_time_ok }; + } + if (responseTime <= tolerances.bad) { + return { label: 'Bad', className: styles.response_time_bad }; + } + return { label: 'Very Bad', className: styles.response_time_very_bad }; + }; + return (
@@ -443,6 +503,35 @@ export default function Status() {
+ {endpoint.responseDetails && ( +
+ + Status Code: + + + + { + endpoint.responseDetails + .status + } + {' '} + { + endpoint.responseDetails + .statusText + } + +
+ )} + {endpoint.responseTime !== undefined && (
{endpoint.responseTime}ms + {endpoint.tolerances.great > + 0 && + endpoint.tolerances.ok > + 0 && + endpoint.tolerances.bad > + 0 && ( + <> + {' ('} + + { + getResponseTimeQuality( + endpoint.responseTime, + endpoint.tolerances, + ).label + } + + {')'} + + )}
)} From 10f2b78cfb55f5781690dcae6ae4cf5c559f8fdd Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 14:54:49 -0500 Subject: [PATCH 07/11] add explicit return types to functions --- packages/frontend/app/routes/status/Status.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index beee1d1fb..a73076a26 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -65,7 +65,7 @@ const ENDPOINTS_TO_MONITOR: EndpointConfig[] = [ const MIN_SPIN_TIME = 0.5; // seconds // JSON syntax highlighter component -const JsonHighlight = ({ data }: { data: unknown }) => { +const JsonHighlight = ({ data }: { data: unknown }): ReactElement => { const highlightJson = (obj: unknown, indent = 0): ReactElement[] => { const elements: ReactElement[] = []; const indentStr = ' '.repeat(indent); @@ -197,7 +197,7 @@ const JsonHighlight = ({ data }: { data: unknown }) => { return
{highlightJson(data)}
; }; -export default function Status() { +export default function Status(): ReactElement { const [endpoints, setEndpoints] = useState( ENDPOINTS_TO_MONITOR.map((ep) => ({ ...ep, @@ -219,7 +219,7 @@ export default function Status() { [], ); - const toggleAccordion = (key: string) => { + const toggleAccordion = (key: string): void => { setExpandedAccordions((prev) => { const newSet = new Set(prev); if (newSet.has(key)) { @@ -231,7 +231,7 @@ export default function Status() { }); }; - const copyToClipboard = async (url: string) => { + const copyToClipboard = async (url: string): Promise => { try { await navigator.clipboard.writeText(url); setCopiedUrl(url); @@ -322,7 +322,7 @@ export default function Status() { } }; - const checkAllEndpoints = async () => { + const checkAllEndpoints = async (): Promise => { const startTime = performance.now(); // Set all endpoints to checking first @@ -346,7 +346,7 @@ export default function Status() { setEndpoints(updatedEndpoints); }; - const checkSingleEndpoint = async (index: number) => { + const checkSingleEndpoint = async (index: number): Promise => { const endpoint = endpoints[index]; const startTime = performance.now(); @@ -378,7 +378,7 @@ export default function Status() { checkAllEndpoints(); }, []); - const getStatusColor = (status: EndpointStatus['status']) => { + const getStatusColor = (status: EndpointStatus['status']): string => { switch (status) { case 'operational': return styles.status_operational; @@ -389,7 +389,7 @@ export default function Status() { } }; - const getStatusCodeColor = (statusCode: number) => { + const getStatusCodeColor = (statusCode: number): string => { if (statusCode >= 100 && statusCode < 200) return styles.status_code_1xx; if (statusCode >= 200 && statusCode < 300) From ba8d31dd7ce83630f7ec081619a21343f09827dd Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Wed, 19 Nov 2025 15:26:04 -0500 Subject: [PATCH 08/11] add copy to clipboard functionality for status code blocks --- .../app/routes/status/Status.module.css | 29 +++++++ .../frontend/app/routes/status/Status.tsx | 83 ++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index 1c70f7f70..650ea34e6 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -382,6 +382,35 @@ max-height: 600px; overflow-y: auto; overflow-x: auto; + position: relative; +} + +.copy_json_button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: transparent; + border: 1px solid var(--border-color, #333); + color: var(--text-secondary, #999); + padding: 0.375rem; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + z-index: 1; +} + +.copy_json_button:hover { + background: var(--bg-secondary, #1a1a1a); + color: var(--text-primary, #ffffff); + border-color: var(--accent1); +} + +.copy_json_button svg { + width: 14px; + height: 14px; } .accordion_content_request { diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index a73076a26..cfd96a4ee 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -241,6 +241,20 @@ export default function Status(): ReactElement { } }; + const copyJsonToClipboard = async ( + data: unknown, + key: string, + ): Promise => { + try { + const jsonString = JSON.stringify(data, null, 2); + await navigator.clipboard.writeText(jsonString); + setCopiedUrl(key); + setTimeout(() => setCopiedUrl(null), 2000); + } catch (err) { + console.error('Failed to copy JSON:', err); + } + }; + const checkEndpoint = async ( endpoint: EndpointStatus, ): Promise => { @@ -255,6 +269,8 @@ export default function Status(): ReactElement { body: requestBody, }; + console.log(`[Status] Request to ${endpoint.name}:`, requestDetails); + try { const response = await fetch(endpoint.url, { method: 'POST', @@ -280,6 +296,11 @@ export default function Status(): ReactElement { body: responseBody, }; + console.log( + `[Status] Response from ${endpoint.name}:`, + responseDetails, + ); + if (response.ok) { return { ...endpoint, @@ -309,6 +330,14 @@ export default function Status(): ReactElement { const responseTimestamp = Date.now(); const responseTime = Math.round(endTime - startTime); + console.error(`[Status] Error checking ${endpoint.name}:`, { + url: endpoint.url, + timestamp: responseTimestamp, + responseTime: `${responseTime}ms`, + error: error instanceof Error ? error.message : 'Unknown error', + details: error, + }); + return { ...endpoint, status: 'down', @@ -674,7 +703,7 @@ export default function Status(): ReactElement { styles.accordion_button_title } > - Request Details + Request
- Response Details + Response - Request Details + Request
+ - Response Details + Response
+ Date: Wed, 19 Nov 2025 15:53:49 -0500 Subject: [PATCH 09/11] add manual console logging of data --- .../app/routes/status/Status.module.css | 42 ++++- .../frontend/app/routes/status/Status.tsx | 162 +++++++++++++----- 2 files changed, 151 insertions(+), 53 deletions(-) diff --git a/packages/frontend/app/routes/status/Status.module.css b/packages/frontend/app/routes/status/Status.module.css index 650ea34e6..c970bc6ce 100644 --- a/packages/frontend/app/routes/status/Status.module.css +++ b/packages/frontend/app/routes/status/Status.module.css @@ -187,6 +187,7 @@ .copy_button:hover { color: var(--text-primary, #ffffff); background: var(--bg-tertiary, #2a2a2a); + border-color: var(--accent1); } .copy_button svg { @@ -385,11 +386,41 @@ position: relative; } -.copy_json_button { - position: absolute; +.json_actions { + position: sticky; top: 0.5rem; - right: 0.5rem; - background: transparent; + float: right; + display: flex; + gap: 0.5rem; + z-index: 10; + margin-bottom: -2rem; +} + +.log_json_button { + background: var(--bg-dark4, #27272c); + border: 1px solid var(--border-color, #333); + color: var(--text-secondary, #999); + padding: 0.375rem 0.625rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.75rem; + font-weight: 500; + transition: all 0.2s ease; + width: 48px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; +} + +.log_json_button:hover { + background: var(--bg-dark5, #323237); + color: var(--text-primary, #ffffff); + border-color: var(--accent1); +} + +.copy_json_button { + background: var(--bg-dark4, #27272c); border: 1px solid var(--border-color, #333); color: var(--text-secondary, #999); padding: 0.375rem; @@ -399,11 +430,10 @@ align-items: center; justify-content: center; transition: all 0.2s ease; - z-index: 1; } .copy_json_button:hover { - background: var(--bg-secondary, #1a1a1a); + background: var(--bg-dark5, #323237); color: var(--text-primary, #ffffff); border-color: var(--accent1); } diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index cfd96a4ee..596e61f20 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -209,6 +209,7 @@ export default function Status(): ReactElement { new Set(), ); const [copiedUrl, setCopiedUrl] = useState(null); + const [loggedItem, setLoggedItem] = useState(null); // Close all accordions on Escape key useKeydown( @@ -255,6 +256,20 @@ export default function Status(): ReactElement { } }; + const logJsonToConsole = ( + data: unknown, + label: string, + key: string, + ): void => { + console.log( + `%c[Status] ${label}:`, + 'color: #3b82f6; font-weight: bold', + data, + ); + setLoggedItem(key); + setTimeout(() => setLoggedItem(null), 2000); + }; + const checkEndpoint = async ( endpoint: EndpointStatus, ): Promise => { @@ -269,8 +284,6 @@ export default function Status(): ReactElement { body: requestBody, }; - console.log(`[Status] Request to ${endpoint.name}:`, requestDetails); - try { const response = await fetch(endpoint.url, { method: 'POST', @@ -296,11 +309,6 @@ export default function Status(): ReactElement { body: responseBody, }; - console.log( - `[Status] Response from ${endpoint.name}:`, - responseDetails, - ); - if (response.ok) { return { ...endpoint, @@ -784,29 +792,59 @@ export default function Status(): ReactElement {
- + + +
- + + +
Date: Thu, 20 Nov 2025 11:09:15 -0500 Subject: [PATCH 10/11] update status page layout --- .../components/Notifications/Notification.tsx | 4 + .../app/routes/status/Status.module.css | 637 ++---------- .../frontend/app/routes/status/Status.tsx | 948 ++---------------- .../frontend/app/stores/NotificationStore.ts | 2 +- 4 files changed, 180 insertions(+), 1411 deletions(-) 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' && ( + + )}

; - body: unknown; - }; - responseDetails?: { - status: number; - statusText: string; - headers: Record; - body: unknown; - }; }; -const ENDPOINTS_TO_MONITOR: EndpointConfig[] = [ - { - name: 'Polling API Info Endpoint', - url: POLLING_API_INFO_ENDPOINT, - tolerances: { - great: 100, - ok: 200, - bad: 300, - }, - }, - { - name: 'RPC Endpoint', - url: RPC_ENDPOINT, - tolerances: { - great: 100, - ok: 200, - bad: 300, - }, - }, -]; - -const MIN_SPIN_TIME = 0.5; // seconds - -// JSON syntax highlighter component -const JsonHighlight = ({ data }: { data: unknown }): ReactElement => { - const highlightJson = (obj: unknown, indent = 0): ReactElement[] => { - const elements: ReactElement[] = []; - const indentStr = ' '.repeat(indent); - - if (obj === null) { - elements.push( - - null - , - ); - } else if (typeof obj === 'boolean') { - elements.push( - - {String(obj)} - , - ); - } else if (typeof obj === 'number') { - elements.push( - - {obj} - , - ); - } else if (typeof obj === 'string') { - elements.push( - - "{obj}" - , - ); - } else if (Array.isArray(obj)) { - elements.push( - - {'[\n'} - , - ); - obj.forEach((item, index) => { - elements.push( - - {indentStr}{' '} - , - ); - elements.push(...highlightJson(item, indent + 1)); - if (index < obj.length - 1) { - elements.push( - - , - , - ); - } - elements.push( - {'\n'}, - ); - }); - elements.push({indentStr}); - elements.push( - - ] - , - ); - } else if (typeof obj === 'object') { - elements.push( - - {'{\n'} - , - ); - const entries = Object.entries(obj); - entries.forEach(([key, value], index) => { - elements.push( - - {indentStr}{' '} - , - ); - elements.push( - - "{key}" - , - ); - elements.push( - - :{' '} - , - ); - elements.push(...highlightJson(value, indent + 1)); - if (index < entries.length - 1) { - elements.push( - - , - , - ); - } - elements.push( - {'\n'}, - ); - }); - elements.push({indentStr}); - elements.push( - - {'}'} - , - ); - } - - return elements; - }; - - return
{highlightJson(data)}
; +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 }, +]; + export default function Status(): ReactElement { const [endpoints, setEndpoints] = useState( - ENDPOINTS_TO_MONITOR.map((ep) => ({ - ...ep, - status: 'checking' as const, - })), - ); - - const [expandedAccordions, setExpandedAccordions] = useState>( - new Set(), + ENDPOINTS.map((ep) => ({ ...ep, status: 'checking' as const })), ); - const [copiedUrl, setCopiedUrl] = useState(null); - const [loggedItem, setLoggedItem] = useState(null); - - // Close all accordions on Escape key - useKeydown( - 'Escape', - () => { - setExpandedAccordions(new Set()); + 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 }, - [], - ); - - const toggleAccordion = (key: string): void => { - setExpandedAccordions((prev) => { - const newSet = new Set(prev); - if (newSet.has(key)) { - newSet.delete(key); - } else { - newSet.add(key); - } - return newSet; - }); - }; - - const copyToClipboard = async (url: string): Promise => { - try { - await navigator.clipboard.writeText(url); - setCopiedUrl(url); - setTimeout(() => setCopiedUrl(null), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - const copyJsonToClipboard = async ( - data: unknown, - key: string, - ): Promise => { - try { - const jsonString = JSON.stringify(data, null, 2); - await navigator.clipboard.writeText(jsonString); - setCopiedUrl(key); - setTimeout(() => setCopiedUrl(null), 2000); - } catch (err) { - console.error('Failed to copy JSON:', err); - } - }; - - const logJsonToConsole = ( - data: unknown, - label: string, - key: string, - ): void => { - console.log( - `%c[Status] ${label}:`, - 'color: #3b82f6; font-weight: bold', - data, - ); - setLoggedItem(key); - setTimeout(() => setLoggedItem(null), 2000); - }; + { + 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(); - const requestTimestamp = Date.now(); - - const requestBody = { type: 'meta' }; - const requestHeaders = { 'Content-Type': 'application/json' }; - const requestDetails = { - method: 'POST', - headers: requestHeaders, - body: requestBody, - }; try { - const response = await fetch(endpoint.url, { - method: 'POST', - headers: requestHeaders, - body: JSON.stringify(requestBody), + await fetch(endpoint.url, { + method: 'HEAD', + mode: 'no-cors', }); const endTime = performance.now(); - const responseTimestamp = Date.now(); const responseTime = Math.round(endTime - startTime); - // Extract response details - const responseBody = await response.json().catch(() => null); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - const responseDetails = { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - body: responseBody, + return { + ...endpoint, + status: 'operational', + responseTime, + lastChecked: new Date(), }; - - if (response.ok) { - return { - ...endpoint, - status: 'operational', - responseTime, - lastChecked: new Date(), - requestTimestamp, - responseTimestamp, - requestDetails, - responseDetails, - }; - } else { - return { - ...endpoint, - status: 'down', - responseTime, - lastChecked: new Date(), - requestTimestamp, - responseTimestamp, - error: `HTTP ${response.status}`, - requestDetails, - responseDetails, - }; - } } catch (error) { const endTime = performance.now(); - const responseTimestamp = Date.now(); const responseTime = Math.round(endTime - startTime); - console.error(`[Status] Error checking ${endpoint.name}:`, { - url: endpoint.url, - timestamp: responseTimestamp, - responseTime: `${responseTime}ms`, - error: error instanceof Error ? error.message : 'Unknown error', - details: error, - }); - return { ...endpoint, status: 'down', responseTime, lastChecked: new Date(), - requestTimestamp, - responseTimestamp, - error: error instanceof Error ? error.message : 'Unknown error', - requestDetails, }; } }; const checkAllEndpoints = async (): Promise => { - const startTime = performance.now(); - - // Set all endpoints to checking first setEndpoints((prev) => prev.map((ep) => ({ ...ep, status: 'checking' as const })), ); - // Check all endpoints in parallel const updatedEndpoints = await Promise.all( endpoints.map(checkEndpoint), ); - // Ensure minimum spinner duration - const elapsedTime = performance.now() - startTime; - const remainingTime = Math.max(0, MIN_SPIN_TIME * 1000 - elapsedTime); - - if (remainingTime > 0) { - await new Promise((resolve) => setTimeout(resolve, remainingTime)); - } - setEndpoints(updatedEndpoints); }; - const checkSingleEndpoint = async (index: number): Promise => { - const endpoint = endpoints[index]; - const startTime = performance.now(); - - // Set status to checking first - setEndpoints((prev) => - prev.map((ep, i) => - i === index ? { ...ep, status: 'checking' as const } : ep, - ), - ); - - // Perform the check - const updatedEndpoint = await checkEndpoint(endpoint); - - // Ensure minimum spinner duration - const elapsedTime = performance.now() - startTime; - const remainingTime = Math.max(0, MIN_SPIN_TIME * 1000 - elapsedTime); - - if (remainingTime > 0) { - await new Promise((resolve) => setTimeout(resolve, remainingTime)); - } - - // Update with result - setEndpoints((prev) => - prev.map((ep, i) => (i === index ? updatedEndpoint : ep)), - ); + 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 => { @@ -426,516 +121,53 @@ export default function Status(): ReactElement { } }; - const getStatusCodeColor = (statusCode: number): string => { - if (statusCode >= 100 && statusCode < 200) - return styles.status_code_1xx; - if (statusCode >= 200 && statusCode < 300) - return styles.status_code_2xx; - if (statusCode >= 300 && statusCode < 400) - return styles.status_code_3xx; - if (statusCode >= 400 && statusCode < 500) - return styles.status_code_4xx; - if (statusCode >= 500 && statusCode < 600) - return styles.status_code_5xx; - return ''; - }; - - const getResponseTimeQuality = ( - responseTime: number, - tolerances: EndpointStatus['tolerances'], - ): { label: string; className: string } => { - if (responseTime <= tolerances.great) { - return { label: 'Great', className: styles.response_time_great }; - } - if (responseTime <= tolerances.ok) { - return { label: 'OK', className: styles.response_time_ok }; - } - if (responseTime <= tolerances.bad) { - return { label: 'Bad', className: styles.response_time_bad }; - } - return { label: 'Very Bad', className: styles.response_time_very_bad }; - }; - return ( -
-
-

System Status

- -
+
-
- {endpoints.map((endpoint, index) => ( -
-
-

- {endpoint.name} -

-
- - - {endpoint.status === 'checking' - ? 'Checking...' - : endpoint.status === 'operational' - ? 'Operational' - : 'Down'} - + {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.status === 'checking' ? ( -
- - - Checking endpoint... - + {endpoint.responseTime !== undefined && ( +
+ {endpoint.responseTime}ms
- ) : ( - <> -
-
- - URL: - - - {endpoint.url} - - -
- - {endpoint.responseDetails && ( -
- - Status Code: - - - - { - endpoint.responseDetails - .status - } - {' '} - { - endpoint.responseDetails - .statusText - } - -
- )} - - {endpoint.responseTime !== undefined && ( -
- - Response Time: - - - {endpoint.responseTime}ms - {endpoint.tolerances.great > - 0 && - endpoint.tolerances.ok > - 0 && - endpoint.tolerances.bad > - 0 && ( - <> - {' ('} - - { - getResponseTimeQuality( - endpoint.responseTime, - endpoint.tolerances, - ).label - } - - {')'} - - )} - -
- )} - - {endpoint.requestTimestamp !== - undefined && ( -
- - Request Dispatched: - - - {endpoint.requestTimestamp} - -
- )} - - {endpoint.responseTimestamp !== - undefined && ( -
- - Response Received: - - - {endpoint.responseTimestamp} - -
- )} - - {endpoint.lastChecked && ( -
- - Last Checked: - - - {endpoint.lastChecked.toLocaleTimeString()} - -
- )} - - {endpoint.error && ( -
- - Error: - - - {endpoint.error} - -
- )} -
- - {(endpoint.requestDetails || - endpoint.responseDetails) && ( -
-
- {endpoint.requestDetails && - (() => { - const jsonString = - JSON.stringify( - endpoint.requestDetails, - null, - 2, - ); - const lineCount = - jsonString.split( - '\n', - ).length; - return ( - - ); - })()} - {endpoint.responseDetails && - (() => { - const jsonString = - JSON.stringify( - endpoint.responseDetails, - null, - 2, - ); - const lineCount = - jsonString.split( - '\n', - ).length; - return ( - - ); - })()} -
-
- {expandedAccordions.has( - `${index}-request`, - ) && - endpoint.requestDetails && ( -
-
- Request -
-
-
- - -
- -
-
- )} - {expandedAccordions.has( - `${index}-response`, - ) && - endpoint.responseDetails && ( -
-
- Response -
-
-
- - -
- -
-
- )} -
-
- )} - )} +
{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 { From 814a9c1235485268397febb24c1a0f0902939422 Mon Sep 17 00:00:00 2001 From: Emily Drake Smith Date: Tue, 2 Dec 2025 15:33:41 -0500 Subject: [PATCH 11/11] add ember to API status page --- packages/frontend/app/routes/status/Status.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/frontend/app/routes/status/Status.tsx b/packages/frontend/app/routes/status/Status.tsx index 1f3b4217d..e46b8b693 100644 --- a/packages/frontend/app/routes/status/Status.tsx +++ b/packages/frontend/app/routes/status/Status.tsx @@ -26,6 +26,10 @@ const ENDPOINTS = [ { 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 {