Add last_updated timestamp to /api/status endpoint by querying MAX(last_seen) across both listing tables. Display it in the HealthIndicator as relative time (e.g. "2h ago") with full date/time in the tooltip on hover.
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
|
import { checkBackendHealth, type HealthStatus, type HealthCheckResult } from '@/services';
|
|
import { Circle, Loader2 } from 'lucide-react';
|
|
|
|
interface HealthIndicatorProps {
|
|
/** How often to check health in milliseconds (default: 30000 = 30s) */
|
|
interval?: number;
|
|
}
|
|
|
|
function formatRelativeTime(isoString: string): string {
|
|
const date = new Date(isoString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
return `${diffDays}d ago`;
|
|
}
|
|
|
|
export function HealthIndicator({ interval = 30000 }: HealthIndicatorProps) {
|
|
const [health, setHealth] = useState<HealthCheckResult>({ status: 'checking' });
|
|
|
|
useEffect(() => {
|
|
// Initial check
|
|
checkBackendHealth().then(setHealth);
|
|
|
|
// Periodic checks
|
|
const intervalId = setInterval(() => {
|
|
checkBackendHealth().then(setHealth);
|
|
}, interval);
|
|
|
|
return () => clearInterval(intervalId);
|
|
}, [interval]);
|
|
|
|
const getStatusColor = (status: HealthStatus) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return 'text-green-500';
|
|
case 'unhealthy':
|
|
return 'text-red-500';
|
|
case 'checking':
|
|
return 'text-muted-foreground';
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: HealthStatus) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return 'Connected';
|
|
case 'unhealthy':
|
|
return 'Disconnected';
|
|
case 'checking':
|
|
return 'Checking...';
|
|
}
|
|
};
|
|
|
|
const getTooltipContent = () => {
|
|
const lines: string[] = [];
|
|
|
|
if (health.status === 'checking') {
|
|
return 'Checking backend connection...';
|
|
}
|
|
|
|
if (health.status === 'healthy') {
|
|
lines.push(`Backend connected (${health.latencyMs}ms)`);
|
|
if (health.lastUpdated) {
|
|
const date = new Date(health.lastUpdated);
|
|
lines.push(`Last update: ${date.toLocaleString()}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
return `Backend unavailable: ${health.error || 'Unknown error'}`;
|
|
};
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center gap-1.5 cursor-default">
|
|
{health.status === 'checking' ? (
|
|
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
|
) : (
|
|
<Circle
|
|
className={`h-2.5 w-2.5 fill-current ${getStatusColor(health.status)}`}
|
|
/>
|
|
)}
|
|
<span className={`text-xs ${getStatusColor(health.status)} hidden sm:inline`}>
|
|
{getStatusLabel(health.status)}
|
|
</span>
|
|
{health.status === 'healthy' && health.lastUpdated && (
|
|
<span className="text-xs text-muted-foreground hidden sm:inline">
|
|
· {formatRelativeTime(health.lastUpdated)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p className="whitespace-pre-line">{getTooltipContent()}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|