From d77d618de0d5198adb579949c788ccac58c0939e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 1 Mar 2026 02:51:18 +0000 Subject: [PATCH] feat(uptime): add pending state handling for monitors; update translations and tests --- frontend/src/locales/de/translation.json | 4 +- frontend/src/locales/en/translation.json | 4 +- frontend/src/locales/es/translation.json | 4 +- frontend/src/locales/fr/translation.json | 4 +- frontend/src/locales/zh/translation.json | 4 +- frontend/src/pages/Uptime.tsx | 21 ++++---- frontend/src/pages/__tests__/Uptime.spec.tsx | 55 ++++++++++++++++++++ 7 files changed, 82 insertions(+), 14 deletions(-) diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json index e8610749..e40b3da1 100644 --- a/frontend/src/locales/de/translation.json +++ b/frontend/src/locales/de/translation.json @@ -423,7 +423,9 @@ "triggerCheck": "Sofortige Gesundheitsprüfung auslösen", "healthCheckTriggered": "Gesundheitsprüfung ausgelöst", "monitorDeleted": "Monitor gelöscht", - "deleteConfirm": "Diesen Monitor löschen? Dies kann nicht rückgängig gemacht werden." + "deleteConfirm": "Diesen Monitor löschen? Dies kann nicht rückgängig gemacht werden.", + "pending": "PRÜFUNG...", + "pendingFirstCheck": "Warten auf erste Prüfung..." }, "domains": { "title": "Domänen", diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index f90c22c3..04eca004 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -498,7 +498,9 @@ "monitorUrl": "URL", "monitorTypeHttp": "HTTP", "monitorTypeTcp": "TCP", - "urlPlaceholder": "https://example.com or tcp://host:port" + "urlPlaceholder": "https://example.com or tcp://host:port", + "pending": "CHECKING...", + "pendingFirstCheck": "Waiting for first check..." }, "domains": { "title": "Domains", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 07593570..a9067bbe 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -423,7 +423,9 @@ "triggerCheck": "Activar verificación de salud inmediata", "healthCheckTriggered": "Verificación de salud activada", "monitorDeleted": "Monitor eliminado", - "deleteConfirm": "¿Eliminar este monitor? Esto no se puede deshacer." + "deleteConfirm": "¿Eliminar este monitor? Esto no se puede deshacer.", + "pending": "VERIFICANDO...", + "pendingFirstCheck": "Esperando primera verificación..." }, "domains": { "title": "Dominios", diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json index 9853dffc..525cec3f 100644 --- a/frontend/src/locales/fr/translation.json +++ b/frontend/src/locales/fr/translation.json @@ -423,7 +423,9 @@ "triggerCheck": "Déclencher une vérification de santé immédiate", "healthCheckTriggered": "Vérification de santé déclenchée", "monitorDeleted": "Moniteur supprimé", - "deleteConfirm": "Supprimer ce moniteur? Cette action est irréversible." + "deleteConfirm": "Supprimer ce moniteur? Cette action est irréversible.", + "pending": "VÉRIFICATION...", + "pendingFirstCheck": "En attente de la première vérification..." }, "domains": { "title": "Domaines", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 09e96cdd..885d64b9 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -423,7 +423,9 @@ "triggerCheck": "触发即时健康检查", "healthCheckTriggered": "健康检查已触发", "monitorDeleted": "监控器已删除", - "deleteConfirm": "删除此监控器?此操作无法撤销。" + "deleteConfirm": "删除此监控器?此操作无法撤销。", + "pending": "检查中...", + "pendingFirstCheck": "等待首次检查..." }, "domains": { "title": "域名", diff --git a/frontend/src/pages/Uptime.tsx b/frontend/src/pages/Uptime.tsx index 25cd4871..6861a767 100644 --- a/frontend/src/pages/Uptime.tsx +++ b/frontend/src/pages/Uptime.tsx @@ -2,7 +2,7 @@ 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 { Activity, ArrowUp, ArrowDown, Settings, X, Pause, RefreshCw, Plus, Loader } from 'lucide-react'; import { toast } from 'react-hot-toast' import { formatDistanceToNow } from 'date-fns'; @@ -64,11 +64,12 @@ const MonitorCard: FC<{ monitor: UptimeMonitor; onEdit: (monitor: UptimeMonitor) ? history.reduce((a, b) => new Date(a.created_at) > new Date(b.created_at) ? a : b) : null + const isPending = monitor.status === 'pending' && (!history || history.length === 0); 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}

@@ -76,12 +77,14 @@ const MonitorCard: FC<{ monitor: UptimeMonitor; onEdit: (monitor: UptimeMonitor)
- {isPaused ? : isUp ? : } - {isPaused ? t('uptime.paused') : monitor.status.toUpperCase()} + : isPending + ? 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 animate-pulse motion-reduce:animate-none' + : isUp + ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' + : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' + }`} data-testid="status-badge" data-status={isPaused ? 'paused' : monitor.status} role="status" aria-label={isPaused ? t('uptime.paused') : isPending ? t('uptime.pending') : isUp ? 'UP' : 'DOWN'}> + {isPaused ? : isPending ?
diff --git a/frontend/src/pages/__tests__/Uptime.spec.tsx b/frontend/src/pages/__tests__/Uptime.spec.tsx index b86ed566..924fb785 100644 --- a/frontend/src/pages/__tests__/Uptime.spec.tsx +++ b/frontend/src/pages/__tests__/Uptime.spec.tsx @@ -230,4 +230,59 @@ describe('Uptime page', () => { expect(screen.getByText('RemoteMon')).toBeInTheDocument() expect(screen.getByText('OtherMon')).toBeInTheDocument() }) + + it('shows CHECKING... state for pending monitor with no history', async () => { + const monitor = { + id: 'm13', name: 'PendingMonitor', url: 'http://example.com', type: 'http', interval: 60, enabled: true, + status: 'pending', last_check: null, latency: 0, max_retries: 3, + } + vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor]) + vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([]) + + renderWithProviders() + await waitFor(() => expect(screen.getByText('PendingMonitor')).toBeInTheDocument()) + const badge = screen.getByTestId('status-badge') + expect(badge).toHaveAttribute('data-status', 'pending') + expect(badge).toHaveAttribute('role', 'status') + expect(badge.textContent).toContain('CHECKING...') + expect(badge.className).toContain('bg-amber-100') + expect(badge.className).toContain('animate-pulse') + expect(screen.getByText('Waiting for first check...')).toBeInTheDocument() + }) + + it('treats pending monitor with heartbeat history as normal (not pending)', async () => { + const monitor = { + id: 'm14', name: 'PendingWithHistory', url: 'http://example.com', type: 'http', interval: 60, enabled: true, + status: 'pending', last_check: new Date().toISOString(), latency: 10, max_retries: 3, + } + const history = [ + { id: 1, monitor_id: 'm14', status: 'up', latency: 10, message: 'OK', created_at: new Date().toISOString() }, + ] + vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor]) + vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue(history) + + renderWithProviders() + await waitFor(() => expect(screen.getByText('PendingWithHistory')).toBeInTheDocument()) + await waitFor(() => { + const badge = screen.getByTestId('status-badge') + expect(badge.textContent).not.toContain('CHECKING...') + expect(badge.className).toContain('bg-green-100') + }) + }) + + it('shows DOWN indicator for down monitor (no regression)', async () => { + const monitor = { + id: 'm15', name: 'DownMonitor', url: 'http://example.com', type: 'http', interval: 60, enabled: true, + status: 'down', last_check: new Date().toISOString(), latency: 0, max_retries: 3, + } + vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor]) + vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([]) + + renderWithProviders() + await waitFor(() => expect(screen.getByText('DownMonitor')).toBeInTheDocument()) + const badge = screen.getByTestId('status-badge') + expect(badge).toHaveAttribute('data-status', 'down') + expect(badge.textContent).toContain('DOWN') + expect(badge.className).toContain('bg-red-100') + }) })