update web ui

This commit is contained in:
2025-11-08 00:09:28 +07:00
parent e0795677e4
commit 42fb17d5e4
18 changed files with 2375 additions and 30 deletions

View File

@@ -0,0 +1,303 @@
import React from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import {Table} from '../../../components/mollecules'
// Define health check response interface based on the provided example
interface HealthCheckEntry {
data: Record<string, any>
duration: string
status: string
tags: string[]
description?: string
}
interface HealthCheckResponse {
status: string
totalDuration: string
entries: Record<string, HealthCheckEntry>
}
// Interface for candle timeframe check data
interface CandleTimeframeCheck {
CheckedTicker: string
CheckedTimeframe: string
StartDate: string
LatestCandleDate?: string
TimeDifference?: string
Status: string
Message: string
}
interface Web3ProxyHealthDetail {
status: string
message: string
data?: Record<string, any>
}
const HealthChecks: React.FC = () => {
const {apiUrl, workerUrl} = useApiUrlStore()
// Use TanStack Query for API health check
const {data: apiHealth, isLoading: isLoadingApi} = useQuery({
queryKey: ['health', 'api'],
queryFn: async () => {
const response = await fetch(`${apiUrl}/health`)
if (!response.ok) throw new Error('Failed to fetch API health')
return response.json() as Promise<HealthCheckResponse>
}
})
// Use TanStack Query for Worker health check
const {data: workerHealth, isLoading: isLoadingWorker} = useQuery({
queryKey: ['health', 'worker'],
queryFn: async () => {
const response = await fetch(`${workerUrl}/health`)
if (!response.ok) throw new Error('Failed to fetch Worker health')
return response.json() as Promise<HealthCheckResponse>
}
})
// Determine overall loading state
const isLoading = isLoadingApi || isLoadingWorker
// Helper function to prepare table data from health response
const prepareHealthData = (
service: string,
health: HealthCheckResponse | null
) => {
if (!health) {
return [
{
service,
component: 'N/A',
status: 'Unreachable',
duration: 'N/A',
tags: 'N/A',
description: 'Service unreachable',
details: null,
},
]
}
// Convert entries to rows for the table
const results: any[] = [];
Object.entries(health.entries).forEach(([key, entry]) => {
// Basic health check entry
const baseEntry = {
service,
component: key,
status: entry.status,
duration: entry.duration,
tags: entry.tags.join(', '),
description: entry.description || '',
details: null,
};
// Add the base entry
results.push(baseEntry);
// Special handling for candle-data to expand timeframe checks
if (key === 'candle-data' && entry.data) {
// Extract timeframe checks
Object.entries(entry.data)
.filter(([dataKey]) => dataKey.startsWith('TimeframeCheck_'))
.forEach(([dataKey, timeframeData]) => {
const tfData = timeframeData as CandleTimeframeCheck;
results.push({
service,
component: `${key} - ${tfData.CheckedTimeframe}`,
status: tfData.Status,
duration: '',
tags: 'candles',
description: tfData.Message,
details: {
Ticker: tfData.CheckedTicker,
LatestCandle: tfData.LatestCandleDate,
TimeDifference: tfData.TimeDifference,
},
});
});
}
// Special handling for Web3Proxy components
if (key === 'web3proxy' && entry.data) {
// Handle Privy check if present
if (entry.data.privy) {
const privyData = entry.data.privy as Web3ProxyHealthDetail;
results.push({
service,
component: `${key} - Privy`,
status: privyData.status,
duration: '',
tags: 'privy, external',
description: privyData.message || '',
details: null,
});
}
// Handle GMX check if present
if (entry.data.gmx) {
const gmxData = entry.data.gmx as Web3ProxyHealthDetail;
const marketDetails: Record<string, any> = {};
// Add market count and response time if available
if (gmxData.data) {
if (gmxData.data.marketCount) {
marketDetails['Market Count'] = gmxData.data.marketCount;
}
if (gmxData.data.responseTimeMs) {
marketDetails['Response Time'] = `${gmxData.data.responseTimeMs}ms`;
}
if (gmxData.data.uiFeeFactor) {
marketDetails['UI Fee Factor'] = gmxData.data.uiFeeFactor;
}
// Add sample markets info (just count for details section)
if (gmxData.data.sampleMarkets && Array.isArray(gmxData.data.sampleMarkets)) {
marketDetails['Sample Markets'] = gmxData.data.sampleMarkets.length;
// If there are sample markets, add the first one's details
if (gmxData.data.sampleMarkets.length > 0) {
const firstMarket = gmxData.data.sampleMarkets[0];
if (firstMarket.indexToken) {
marketDetails['Example Market'] = firstMarket.indexToken;
}
}
}
}
results.push({
service,
component: `${key} - GMX`,
status: gmxData.status,
duration: '',
tags: 'gmx, external',
description: gmxData.message || '',
details: Object.keys(marketDetails).length > 0 ? marketDetails : null,
});
}
// Add version info if available
if (entry.data.version) {
const versionDetails: Record<string, any> = {
Version: entry.data.version
};
if (entry.data.timestamp) {
versionDetails['Last Updated'] = entry.data.timestamp;
}
results.push({
service,
component: `${key} - Version`,
status: entry.status,
duration: '',
tags: 'version',
description: `Web3Proxy Version: ${entry.data.version}`,
details: versionDetails,
});
}
}
});
return results;
}
// Combine all health check data for display
const healthData = [
...prepareHealthData('Managing API', apiHealth || null),
...prepareHealthData('Managing Worker', workerHealth || null),
]
// Define columns for the table
const columns = React.useMemo(
() => [
{
Header: 'Service',
accessor: 'service',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Component',
accessor: 'component',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Status',
accessor: 'status',
Cell: ({value}: { value: string }) => (
<span
className={`badge ${
value === 'Healthy' || value === 'healthy'
? 'badge-success'
: value === 'Unreachable' || value === 'unhealthy'
? 'badge-error'
: 'badge-warning'
}`}
>
{value.charAt(0).toUpperCase() + value.slice(1)}
</span>
),
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Description',
accessor: 'description',
disableSortBy: true,
disableFilters: true,
Cell: ({value, row}: any) => (
<div>
<div>{value}</div>
{row.original.details && (
<div className="text-xs mt-1 opacity-80">
{Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map(
([key, val]) => (
<div key={key}>
<span className="font-semibold">{key}:</span> {String(val)}
</div>
)
)}
</div>
)}
</div>
),
},
{
Header: 'Duration',
accessor: 'duration',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Tags',
accessor: 'tags',
disableSortBy: true,
disableFilters: true,
},
],
[]
)
return (
<div className="container mx-auto">
<h2 className="text-xl font-bold mb-4">System Health Status</h2>
{isLoading ? (
<progress className="progress progress-primary w-56"></progress>
) : (
<Table
columns={columns}
data={healthData}
showPagination={true}
/>
)}
</div>
)
}
export default HealthChecks