import React, { useState, useEffect, useCallback, useRef } from 'react'; import { FiAward, FiPercent, FiActivity, FiCheckCircle, FiClock, FiGlobe, FiLock } from 'react-icons/fi'; import { DateTime } from 'luxon'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; function getStatusColor(validCert) { if (validCert === false) return 'bg-red-600'; if (validCert === true) return 'bg-green-600'; return 'bg-gray-400'; } function getHeartbeatColor(status) { if (status === 1) return 'bg-green-600'; // UP if (status === 0) return 'bg-yellow-400'; // PENDING if (status === 2) return 'bg-red-600'; // DOWN return 'bg-gray-300'; } function getCertBg(days) { if (typeof days !== 'number') return 'bg-gray-400 dark:bg-gray-600'; if (days < 0) return 'bg-red-600 animate-pulse border-2 border-red-500 dark:bg-red-800'; if (days < 5) return 'bg-red-600 dark:bg-red-800'; if (days <= 15) return 'bg-yellow-400 dark:bg-yellow-500 text-gray-900'; return 'bg-green-600 dark:bg-green-700'; } function getCertText(days) { if (typeof days !== 'number') return 'Cert Exp'; if (days < 0) return 'Cert Expired!'; return 'Cert Exp'; } function getAvgPingBg(avgPing) { if (typeof avgPing !== 'number') return 'bg-gray-300 dark:bg-gray-700'; if (avgPing <= 80) return 'bg-green-600 text-white dark:bg-green-700 dark:text-white'; if (avgPing <= 200) return 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-gray-900'; return 'bg-red-600 text-white dark:bg-red-800 dark:text-white'; } function getUptime24hBg(uptime) { if (typeof uptime !== 'number') return 'bg-gray-300 dark:bg-gray-700'; if (uptime >= 99) return 'bg-green-600 text-white dark:bg-green-700 dark:text-white'; if (uptime >= 95) return 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-gray-900'; return 'bg-red-600 text-white dark:bg-red-800 dark:text-white'; } function formatLocalTime(rawTime, zone = 'utc') { if (!rawTime) return ''; const dt = DateTime.fromFormat(rawTime, 'yyyy-MM-dd HH:mm:ss.SSS', { zone: 'utc' }); const localDt = dt.isValid ? dt.setZone(zone) : null; return localDt && localDt.isValid ? localDt.toFormat('dd-MM-yyyy, HH:mm:ss') : 'Invalid DateTime'; } function HeartbeatPopup({ hb, userZone, monitor }) { const localTime = hb ? formatLocalTime(hb.time, userZone) : ''; const utcTime = hb ? formatLocalTime(hb.time, 'utc') : ''; return (
{hb.status === 1 ? 'Up' : 'Down'}
Local: {localTime}
UTC: {utcTime}
Ping: {hb.ping ?? '--'} ms
Uptime 24h: {monitor.uptime24h?.toFixed(2) ?? '--'}%
Cert valid for: {monitor.certExpiryDaysRemaining ?? '--'} days
); } export default function UptimeStatusIsland() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [openBadge, setOpenBadge] = useState({}); const badgeRefs = useRef({}); const [userZone, setUserZone] = useState(null); const [userLocale, setUserLocale] = useState(null); const [secondsToNextUpdate, setSecondsToNextUpdate] = useState(null); const [isMobile, setIsMobile] = useState(false); const [showLeftShadow, setShowLeftShadow] = useState(false); const heartbeatContainerRefs = useRef({}); useEffect(() => { setUserZone(Intl.DateTimeFormat().resolvedOptions().timeZone); setUserLocale(navigator.language || 'en-US'); }, []); // Helper: get the current 30-min window key (e.g., '2024-06-07T12:00') const getCurrent30MinKey = () => { const now = new Date(); now.setSeconds(0, 0); const min = now.getMinutes(); now.setMinutes(Math.floor(min / 30) * 30); return now.toISOString().slice(0, 16); // 'YYYY-MM-DDTHH:mm' }; // Helper: get seconds to next 30-min mark const getSecondsToNext30Min = () => { const now = new Date(); const minutes = now.getMinutes(); const seconds = now.getSeconds(); const ms = now.getMilliseconds(); const next30 = Math.ceil((minutes + 0.01) / 30) * 30; let next = new Date(now); next.setMinutes(next30, 0, 0); if (next <= now) { next.setMinutes(next.getMinutes() + 30); } return Math.floor((next - now) / 1000); }; useEffect(() => { const update = () => setSecondsToNextUpdate(getSecondsToNext30Min()); update(); const interval = setInterval(update, 1000); return () => clearInterval(interval); }, []); // On mount, check sessionStorage for valid cached data useEffect(() => { const cacheKey = 'uptimeStatusCache'; const cache = sessionStorage.getItem(cacheKey); if (cache) { try { const { key, data: cachedData } = JSON.parse(cache); if (key === getCurrent30MinKey() && cachedData) { setData(cachedData); setLoading(false); return; } } catch {} } // If no valid cache, fetch immediately fetchDataImmediate(); }, []); // Helper: fetch and cache immediately (used on mount if no cache) const fetchDataImmediate = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch('/api/uptime'); if (!response.ok) throw new Error('Failed to fetch uptime data'); const json = await response.json(); setData(json); // Cache with current 30-min key const cacheKey = 'uptimeStatusCache'; sessionStorage.setItem(cacheKey, JSON.stringify({ key: getCurrent30MinKey(), data: json })); } catch (err) { setError(err.message || 'Unknown error'); } finally { setLoading(false); } }, []); // Scheduled fetch logic (every 30 min, update cache) const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch('/api/uptime'); if (!response.ok) throw new Error('Failed to fetch uptime data'); const json = await response.json(); setData(json); // Cache with current 30-min key const cacheKey = 'uptimeStatusCache'; sessionStorage.setItem(cacheKey, JSON.stringify({ key: getCurrent30MinKey(), data: json })); } catch (err) { setError(err.message || 'Unknown error'); } finally { setLoading(false); } }, []); useEffect(() => { const now = new Date(); const minutes = now.getMinutes(); const seconds = now.getSeconds(); const ms = now.getMilliseconds(); const next30 = Math.ceil((minutes + 0.01) / 30) * 30; let next = new Date(now); next.setMinutes(next30, 0, 0); if (next <= now) { next.setMinutes(next.getMinutes() + 30); } const msToNext = next - now; const timeout = setTimeout(() => { fetchData(); const interval = setInterval(fetchData, 30 * 60 * 1000); fetchData.interval = interval; }, msToNext); return () => { clearTimeout(timeout); if (fetchData.interval) clearInterval(fetchData.interval); }; }, [fetchData]); const toggleBadge = (monitorId, badge) => { setOpenBadge((prev) => ({ ...prev, [monitorId]: { ...prev[monitorId], [badge]: !prev[monitorId]?.[badge], }, })); }; useEffect(() => { function handleClick(e) { let clickedInside = false; Object.values(badgeRefs.current).forEach((monitorBadges) => { Object.values(monitorBadges || {}).forEach((ref) => { if (ref && ref.current && ref.current.contains(e.target)) { clickedInside = true; } }); }); if (!clickedInside) setOpenBadge({}); } document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, []); const getBadgeRef = (monitorId, badge) => { if (!badgeRefs.current[monitorId]) badgeRefs.current[monitorId] = {}; if (!badgeRefs.current[monitorId][badge]) badgeRefs.current[monitorId][badge] = React.createRef(); return badgeRefs.current[monitorId][badge]; }; const totalBarSeconds = 30 * 60; const barValue = secondsToNextUpdate !== null ? (totalBarSeconds - secondsToNextUpdate) : 0; const barPercent = (barValue / totalBarSeconds) * 100; const barGradient = 'linear-gradient(90deg, #0161ef 0%, #0154cf 100%)'; // Format countdown as 'X min XX sec' or 'XX sec' let countdownText = '--'; if (typeof secondsToNextUpdate === 'number' && secondsToNextUpdate >= 0) { if (secondsToNextUpdate >= 60) { const min = Math.floor(secondsToNextUpdate / 60); const sec = secondsToNextUpdate % 60; countdownText = `${min} min ${sec.toString().padStart(2, '0')} sec`; } else { countdownText = `${secondsToNextUpdate} sec`; } } useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 640); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // Track scroll position for left shadow on mobile heartbeat bar const handleHeartbeatScroll = (monitorId, e) => { if (!isMobile) return; setShowLeftShadow(e.target.scrollLeft > 0); }; return (
Next update in {countdownText}
{loading ? (
Loading...
) : error ? (
{error}
) : data && data.publicGroupList && data.publicGroupList.length > 0 ? ( data.publicGroupList.map((group) => (

{group.name}

    {group.monitorList.map((monitor) => (
  • {isMobile ? ( <>
    {monitor.name}
    {monitor.certExpiryDaysRemaining !== undefined && ( toggleBadge(monitor.id, 'cert')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.cert && ( {monitor.certExpiryDaysRemaining < 0 ? 'Expired!' : `Expires in ${monitor.certExpiryDaysRemaining} days`} )} )} {monitor.avgPing !== undefined && ( toggleBadge(monitor.id, 'avgPing')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.avgPing && ( {monitor.avgPing} ms )} )} {monitor.uptime24h !== undefined && ( toggleBadge(monitor.id, 'uptime')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.uptime && ( {monitor.uptime24h.toFixed(1)}% )} )}
    ) : (
    {monitor.name}
    {monitor.certExpiryDaysRemaining !== undefined && ( <> {getCertText(monitor.certExpiryDaysRemaining)} toggleBadge(monitor.id, 'cert')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.cert && ( {monitor.certExpiryDaysRemaining < 0 ? 'Expired!' : `Expires in ${monitor.certExpiryDaysRemaining} days`} )} )}
    {monitor.avgPing !== undefined && ( <> Avg: {monitor.avgPing} ms toggleBadge(monitor.id, 'avgPing')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.avgPing && ( {monitor.avgPing} ms )} )}
    {monitor.uptime24h !== undefined && ( <> 24h: {monitor.uptime24h.toFixed(1)}% toggleBadge(monitor.id, 'uptime')} style={{ cursor: 'pointer', position: 'relative' }} > {openBadge[monitor.id]?.uptime && ( {monitor.uptime24h.toFixed(1)}% )} )}
    )} {monitor.heartbeatHistory && monitor.heartbeatHistory.length > 0 && (
    { if (el && isMobile) heartbeatContainerRefs.current[monitor.id] = el; }} onScroll={e => handleHeartbeatScroll(monitor.id, e)} > {/* Left shadow overlay for mobile, only if scrolled */} {isMobile && showLeftShadow && (
    )}
    {(() => { const hbArr = monitor.heartbeatHistory ?? []; const slice = isMobile ? hbArr.slice(-10) : hbArr.slice(-40); // Show most recent first (left), so reverse the slice const ordered = slice; return ordered.map((hb, i) => ( } placement="top" animation="shift-away" theme="devnet" delay={[0, 0]} > )); })()}
    )}
  • ))}
)) ) : (
No status data available
)}
); }