buster/apps/web/src/routes/app/healthcheck.tsx

376 lines
14 KiB
TypeScript

import type { HealthCheckResponse } from '@buster/server-shared/healthcheck';
import { createFileRoute } from '@tanstack/react-router';
import { useHealthcheck } from '@/api/buster_rest/healthcheck/queryRequests';
import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
import type { RustApiError } from '@/api/errors';
import { useGetSupabaseUser } from '@/context/Supabase';
export const Route = createFileRoute('/app/healthcheck')({
component: RouteComponent,
});
function RouteComponent() {
const { data, isLoading, error } = useHealthcheck();
const supabaseUser = useGetSupabaseUser();
const user = useGetUserBasicInfo();
if (isLoading) {
return <LoadingState />;
}
if (error) {
return <ErrorState error={error} />;
}
if (!data) {
return <ErrorState error={new Error('No data received')} />;
}
return <HealthcheckDashboard data={data} supabaseUser={supabaseUser} user={user} />;
}
function LoadingState() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Checking system health...</p>
</div>
</div>
);
}
function ErrorState({ error }: { error: Error | RustApiError }) {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 to-white flex items-center justify-center">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full mx-4">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">Health Check Failed</h2>
<p className="text-gray-600">Unable to retrieve system status</p>
</div>
</div>
<div className="bg-red-50 border border-red-200 rounded-md p-3">
<p className="text-sm text-red-800">{error.message}</p>
</div>
</div>
</div>
);
}
function HealthcheckDashboard({
data,
supabaseUser,
user,
}: {
data: HealthCheckResponse;
supabaseUser: ReturnType<typeof useGetSupabaseUser>;
user: ReturnType<typeof useGetUserBasicInfo>;
}) {
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy':
case 'pass':
return 'text-green-600 bg-green-100';
case 'degraded':
case 'warn':
return 'text-yellow-600 bg-yellow-100';
case 'unhealthy':
case 'fail':
return 'text-red-600 bg-red-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'healthy':
case 'pass':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
);
case 'degraded':
case 'warn':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
);
case 'unhealthy':
case 'fail':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
default:
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
}
};
const formatUptime = (seconds: number) => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`;
}
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
};
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString();
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">System Health Dashboard</h1>
<p className="text-gray-600">Real-time monitoring of system components</p>
</div>
{/* User Data Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Supabase User Data */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-4 h-4 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
Supabase User
</h3>
<div className="bg-gray-50 rounded-md p-4">
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
{JSON.stringify(supabaseUser, null, 2)}
</pre>
</div>
</div>
{/* User Basic Info */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-4 h-4 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
User Basic Info
</h3>
<div className="bg-gray-50 rounded-md p-4">
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
{JSON.stringify(user, null, 2)}
</pre>
</div>
</div>
</div>
{/* Overall Status Card */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center mr-4 ${getStatusColor(
data.status
)}`}
>
{getStatusIcon(data.status)}
</div>
<div>
<h2 className="text-2xl font-semibold text-gray-900 capitalize">{data.status}</h2>
<p className="text-gray-600">Overall System Status</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Last checked</p>
<p className="text-gray-900 font-medium">{formatTimestamp(data.timestamp)}</p>
</div>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Uptime</p>
<p className="text-xl font-semibold text-gray-900">{formatUptime(data.uptime)}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 011 1v1a1 1 0 01-1 1h-1v12a2 2 0 01-2 2H6a2 2 0 01-2-2V7H3a1 1 0 01-1-1V5a1 1 0 011-1h4z"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Version</p>
<p className="text-xl font-semibold text-gray-900">{data.version}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Environment</p>
<p className="text-xl font-semibold text-gray-900 capitalize">{data.environment}</p>
</div>
</div>
</div>
</div>
{/* Component Checks */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-6">Component Health Checks</h3>
<div className="space-y-4">
{Object.entries(data.checks).map(([componentName, check]) => (
<div key={componentName} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStatusColor(
check.status
)}`}
>
{getStatusIcon(check.status)}
</div>
<div>
<h4 className="font-medium text-gray-900 capitalize">{componentName}</h4>
{check.message && <p className="text-sm text-gray-600">{check.message}</p>}
</div>
</div>
<div className="text-right">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${getStatusColor(
check.status
)}`}
>
{check.status}
</span>
{check.responseTime && (
<p className="text-sm text-gray-500 mt-1">{check.responseTime}ms</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center text-gray-500 text-sm">
<p>System health is monitored continuously. Data refreshes automatically.</p>
</div>
</div>
</div>
);
}