import { useMemo, useState, type FC, type FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getMonitors, getMonitorHistory, updateMonitor, deleteMonitor, checkMonitor, createMonitor, syncMonitors, UptimeMonitor } from '../api/uptime'; import { Activity, ArrowUp, ArrowDown, Settings, X, Pause, RefreshCw, Plus } from 'lucide-react'; import { toast } from 'react-hot-toast' import { formatDistanceToNow } from 'date-fns'; const MonitorCard: FC<{ monitor: UptimeMonitor; onEdit: (monitor: UptimeMonitor) => void; t: (key: string, options?: Record) => string }> = ({ monitor, onEdit, t }) => { const { data: history } = useQuery({ queryKey: ['uptimeHistory', monitor.id], queryFn: () => getMonitorHistory(monitor.id, 60), refetchInterval: 60000, }); const queryClient = useQueryClient() const deleteMutation = useMutation({ mutationFn: async (id: string) => { return await deleteMonitor(id) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitors'] }) toast.success(t('uptime.monitorDeleted')) }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : t('uptime.failedToDeleteMonitor')) } }) const toggleMutation = useMutation({ mutationFn: async ({ id, enabled }: { id: string; enabled: boolean }) => { return await updateMonitor(id, { enabled }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitors'] }) }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : t('uptime.failedToUpdateMonitor')) } }) const checkMutation = useMutation({ mutationFn: async (id: string) => { return await checkMonitor(id) }, onSuccess: () => { toast.success(t('uptime.healthCheckTriggered')) // Refetch monitor and history after a short delay to get updated results setTimeout(() => { queryClient.invalidateQueries({ queryKey: ['monitors'] }) queryClient.invalidateQueries({ queryKey: ['uptimeHistory', monitor.id] }) }, 2000) }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : t('uptime.failedToTriggerCheck')) } }) const [showMenu, setShowMenu] = useState(false) // Determine current status from most recent heartbeat when available const latestBeat = history && history.length > 0 ? history.reduce((a, b) => new Date(a.created_at) > new Date(b.created_at) ? a : b) : null const isUp = latestBeat ? latestBeat.status === 'up' : monitor.status === 'up'; const isPaused = monitor.enabled === false; return (
{/* Top Row: Name (left), Badge (center-right), Settings (right) */}

{monitor.name}

{isPaused ? : isUp ? : } {isPaused ? t('uptime.paused') : monitor.status.toUpperCase()}
{showMenu && (
)}
{/* URL and Type */}
{monitor.url} {monitor.type.toUpperCase()}
{t('uptime.latency')}
{monitor.latency}ms
{t('uptime.lastCheck')}
{monitor.last_check ? formatDistanceToNow(new Date(monitor.last_check), { addSuffix: true }) : t('uptime.never')}
{/* Heartbeat Bar (Last 60 checks / 1 Hour) */}
{/* Fill with empty bars if not enough history to keep alignment right-aligned */} {Array.from({ length: Math.max(0, 60 - (history?.length || 0)) }).map((_, i) => (
))} {history?.slice().reverse().map((beat: { status: string; created_at: string; latency: number; message: string }, i: number) => (
))} {(!history || history.length === 0) && (
{t('uptime.noHistoryAvailable')}
)}
); }; const EditMonitorModal: FC<{ monitor: UptimeMonitor; onClose: () => void; t: (key: string) => string }> = ({ monitor, onClose, t }) => { const queryClient = useQueryClient(); const [name, setName] = useState(monitor.name || '') const [maxRetries, setMaxRetries] = useState(monitor.max_retries || 3); const [interval, setInterval] = useState(monitor.interval || 60); const mutation = useMutation({ mutationFn: (data: Partial) => updateMonitor(monitor.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitors'] }); onClose(); }, }); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutation.mutate({ name, max_retries: maxRetries, interval }); }; return (

{t('uptime.configureMonitor')}

setName(e.target.value)} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
{ const v = parseInt(e.target.value) setMaxRetries(Number.isNaN(v) ? 0 : v) }} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />

{t('uptime.maxRetriesHelper')}

{ const v = parseInt(e.target.value) setInterval(Number.isNaN(v) ? 0 : v) }} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
); }; const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }> = ({ onClose, t }) => { const queryClient = useQueryClient(); const [name, setName] = useState(''); const [url, setUrl] = useState(''); const [type, setType] = useState<'http' | 'tcp'>('http'); const [interval, setInterval] = useState(60); const [maxRetries, setMaxRetries] = useState(3); const mutation = useMutation({ mutationFn: (data: { name: string; url: string; type: string; interval?: number; max_retries?: number }) => createMonitor(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitors'] }); toast.success(t('uptime.monitorCreated')); onClose(); }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : t('errors.genericError')); }, }); const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (!name.trim() || !url.trim()) return; mutation.mutate({ name: name.trim(), url: url.trim(), type, interval, max_retries: maxRetries }); }; return (

{t('uptime.createMonitor')}

setName(e.target.value)} required className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="My Service" />
setUrl(e.target.value)} required className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder={t('uptime.urlPlaceholder')} />
{ const v = parseInt(e.target.value); setInterval(Number.isNaN(v) ? 60 : v); }} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
{ const v = parseInt(e.target.value); setMaxRetries(Number.isNaN(v) ? 3 : v); }} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />

{t('uptime.maxRetriesHelper')}

); }; const Uptime: FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); const { data: monitors, isLoading } = useQuery({ queryKey: ['monitors'], queryFn: getMonitors, refetchInterval: 30000, }); const [editingMonitor, setEditingMonitor] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const syncMutation = useMutation({ mutationFn: () => syncMonitors(), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['monitors'] }); toast.success(data.message || t('uptime.syncComplete')); }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : t('errors.genericError')); }, }); // Sort monitors alphabetically by name const sortedMonitors = useMemo(() => { if (!monitors) return []; return [...monitors].sort((a, b) => (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()) ); }, [monitors]); const proxyHostMonitors = useMemo(() => sortedMonitors.filter(m => m.proxy_host_id), [sortedMonitors]); const remoteServerMonitors = useMemo(() => sortedMonitors.filter(m => m.remote_server_id), [sortedMonitors]); const otherMonitors = useMemo(() => sortedMonitors.filter(m => !m.proxy_host_id && !m.remote_server_id), [sortedMonitors]); if (isLoading) { return
{t('uptime.loadingMonitors')}
; } return (

{t('uptime.title')}

{t('uptime.autoRefreshing')}
{sortedMonitors.length === 0 ? (
{t('uptime.noMonitorsFound')}
) : ( <> {proxyHostMonitors.length > 0 && (

{t('uptime.proxyHosts')}

{proxyHostMonitors.map((monitor) => ( ))}
)} {remoteServerMonitors.length > 0 && (

{t('uptime.remoteServers')}

{remoteServerMonitors.map((monitor) => ( ))}
)} {otherMonitors.length > 0 && (

{t('uptime.otherMonitors')}

{otherMonitors.map((monitor) => ( ))}
)} )} {editingMonitor && ( setEditingMonitor(null)} t={t} /> )} {showCreateModal && ( setShowCreateModal(false)} t={t} /> )}
); }; export default Uptime;