From a084480695c4076996fe2fc39c0f9bdff0fe7b74 Mon Sep 17 00:00:00 2001 From: becarta Date: Sun, 8 Jun 2025 02:43:05 +0200 Subject: [PATCH] Update dependencies and enhance UptimeStatusIsland component with timezone and locale handling - Added luxon library for improved date handling. - Updated UptimeStatusIsland component to format and display local time for heartbeats. - Enhanced state management to track user timezone and locale. - Ensured UTC formatting for timestamps in API responses. --- package-lock.json | 85 +++++++++++++++------------ package.json | 1 + src/components/UptimeStatusIsland.jsx | 48 +++++++++++---- src/pages/api/uptime.ts | 20 +++++-- src/utils/utils.ts | 34 ++++++++++- 5 files changed, 132 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3f4990..2b085c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "form-data": "^4.0.2", "limax": "4.1.0", "lodash.merge": "^4.6.2", + "luxon": "^3.6.1", "node-fetch": "^3.3.2", "nodemailer": "^6.10.0", "rate-limiter-flexible": "^5.0.5", @@ -574,14 +575,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -731,18 +732,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -758,25 +759,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -816,14 +817,14 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -857,13 +858,13 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3798,9 +3799,10 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -7152,6 +7154,15 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -10642,9 +10653,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.21.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", - "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 6bfd853..dc8e987 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "form-data": "^4.0.2", "limax": "4.1.0", "lodash.merge": "^4.6.2", + "luxon": "^3.6.1", "node-fetch": "^3.3.2", "nodemailer": "^6.10.0", "rate-limiter-flexible": "^5.0.5", diff --git a/src/components/UptimeStatusIsland.jsx b/src/components/UptimeStatusIsland.jsx index 9011426..9077987 100644 --- a/src/components/UptimeStatusIsland.jsx +++ b/src/components/UptimeStatusIsland.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { FiAward, FiPercent, FiActivity } from 'react-icons/fi'; +import { DateTime } from 'luxon'; function getStatusColor(validCert) { if (validCert === false) return 'bg-red-600'; @@ -38,19 +39,31 @@ function getAvgPingBg(avgPing) { function getUptime24hBg(uptime) { if (typeof uptime !== 'number') return 'bg-gray-300 dark:bg-gray-700'; - if (uptime >= 99.9) return 'bg-green-600 text-white dark:bg-green-700 dark:text-white'; - if (uptime >= 99) return 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-gray-900'; + 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(utcTime) { + if (!utcTime) return ''; + const dt = DateTime.fromISO(utcTime, { zone: 'utc' }); + // Always show in UTC, format: dd-MM-yyyy, HH:mm:ss UTC + return dt.toFormat('dd-MM-yyyy, HH:mm:ss') + ' UTC'; +} + export default function UptimeStatusIsland() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Track open badge for each monitor by monitor.id and badge type const [openBadge, setOpenBadge] = useState({}); - // Refs for each badge popup const badgeRefs = useRef({}); + const [userZone, setUserZone] = useState(null); + const [userLocale, setUserLocale] = useState(null); + + useEffect(() => { + setUserZone(Intl.DateTimeFormat().resolvedOptions().timeZone); + setUserLocale(navigator.language || 'en-US'); + }, []); const fetchData = useCallback(async () => { setLoading(true); @@ -68,7 +81,9 @@ export default function UptimeStatusIsland() { }, []); useEffect(() => { - fetchData(); + fetchData(); // initial fetch + const interval = setInterval(fetchData, 300000); // fetch every 5 minutes + return () => clearInterval(interval); }, [fetchData]); // Helper to toggle badge info @@ -106,6 +121,11 @@ export default function UptimeStatusIsland() { return badgeRefs.current[monitorId][badge]; }; + // Only render cards when timezone and locale are available + if (!userZone || !userLocale) { + return
Loading timezone...
; + } + return (
@@ -161,27 +181,28 @@ export default function UptimeStatusIsland() { {monitor.avgPing !== undefined && ( <> {/* Desktop: full badge */} - - Avg.: {monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms + + Avg: {monitor.avgPing} ms {/* Mobile: icon badge with click */} toggleBadge(monitor.id, 'avg')} + onClick={() => toggleBadge(monitor.id, 'avgPing')} style={{ cursor: 'pointer', position: 'relative' }} > - {openBadge[monitor.id]?.avg && ( + {openBadge[monitor.id]?.avgPing && ( - {monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms + {monitor.avgPing} ms )} )}
- {/* 24h (right, col-span-1) */} + {/* 24h Uptime (right, col-span-1) */}
{monitor.uptime24h !== undefined && ( <> @@ -214,10 +235,11 @@ export default function UptimeStatusIsland() { {Array.from({ length: 40 }).map((_, i) => { const hbArr = monitor.heartbeatHistory ?? []; const hb = hbArr.slice(-40)[i]; + const localTime = hb ? formatLocalTime(hb.time) : ''; return ( ); diff --git a/src/pages/api/uptime.ts b/src/pages/api/uptime.ts index 7ae8cee..4118cc5 100644 --- a/src/pages/api/uptime.ts +++ b/src/pages/api/uptime.ts @@ -7,7 +7,7 @@ const STATUS_PAGE_SLUG = '365devnet'; // all lowercase interface Heartbeat { status: number; // 0=DOWN, 1=UP, 2=PENDING, 3=MAINTENANCE - time: string; + time: string; // UTC ISO string msg: string; ping: number | null; important?: boolean; @@ -28,7 +28,7 @@ interface Monitor { uptimePercent?: number; currentPing?: number; avgPing?: number; - lastChecked?: string; + lastChecked?: string; // UTC ISO string uptime24h?: number; } @@ -56,6 +56,12 @@ const fetchWithTimeout = async (url: string, options: RequestInit, timeout = 100 } }; +// Helper function to ensure a date string is in UTC ISO format +function ensureUTC(dateString: string): string { + const date = new Date(dateString); + return date.toISOString(); +} + export const GET: APIRoute = async () => { try { if (!UPTIME_KUMA_URL) { @@ -107,7 +113,11 @@ export const GET: APIRoute = async () => { for (const group of data.publicGroupList) { for (const monitor of group.monitorList) { const hbArr = heartbeatList[monitor.id.toString()] || []; - monitor.heartbeatHistory = hbArr; + // Ensure all heartbeat times are in UTC + monitor.heartbeatHistory = hbArr.map(hb => ({ + ...hb, + time: ensureUTC(hb.time) + })); // Uptime % (last 40 heartbeats) if (hbArr.length > 0) { const last40 = hbArr.slice(-40); @@ -118,8 +128,8 @@ export const GET: APIRoute = async () => { // Average ping (last 40 heartbeats) const pings = last40.map(hb => hb.ping).filter(p => typeof p === 'number') as number[]; monitor.avgPing = pings.length > 0 ? Math.round((pings.reduce((a, b) => a + b, 0) / pings.length) * 10) / 10 : undefined; - // Last checked - monitor.lastChecked = last40[last40.length - 1]?.time ?? undefined; + // Last checked (ensure UTC) + monitor.lastChecked = last40[last40.length - 1]?.time ? ensureUTC(last40[last40.length - 1].time) : undefined; } else { monitor.uptimePercent = undefined; monitor.currentPing = undefined; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e2ed559..f5af419 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,14 +1,46 @@ import { I18N } from 'astrowind:config'; +// Format dates in user's local timezone export const formatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(I18N?.language, { year: 'numeric', month: 'short', day: 'numeric', - timeZone: 'UTC', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', +}); + +// Format dates in user's local timezone with full date and time +export const fullDateFormatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(I18N?.language, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', }); export const getFormattedDate = (date: Date): string => (date ? formatter.format(date) : ''); +export const getFullFormattedDate = (date: Date): string => (date ? fullDateFormatter.format(date) : ''); + +// Get user's timezone +export const getUserTimezone = (): string => { + if (typeof window !== 'undefined') { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } + return 'UTC'; +}; + +// Get user's locale +export const getUserLocale = (): string => { + if (typeof window !== 'undefined') { + return navigator.language; + } + return I18N?.language || 'en'; +}; + export const trim = (str = '', ch?: string) => { let start = 0, end = str.length || 0;