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.
This commit is contained in:
85
package-lock.json
generated
85
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"limax": "4.1.0",
|
"limax": "4.1.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
"luxon": "^3.6.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"rate-limiter-flexible": "^5.0.5",
|
"rate-limiter-flexible": "^5.0.5",
|
||||||
@@ -574,14 +575,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.26.2",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.0.0"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -731,18 +732,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -758,25 +759,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
|
||||||
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
|
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.26.9",
|
"@babel/template": "^7.27.2",
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||||
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
|
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -816,14 +817,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/parser": "^7.26.9",
|
"@babel/parser": "^7.27.2",
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -857,13 +858,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
||||||
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.25.9",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
"@babel/helper-validator-identifier": "^7.25.9"
|
"@babel/helper-validator-identifier": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -3798,9 +3799,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.5",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||||
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
|
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@@ -7152,6 +7154,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
@@ -10642,9 +10653,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.21.1",
|
"version": "6.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
@@ -39,6 +39,7 @@
|
|||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"limax": "4.1.0",
|
"limax": "4.1.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
"luxon": "^3.6.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"rate-limiter-flexible": "^5.0.5",
|
"rate-limiter-flexible": "^5.0.5",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { FiAward, FiPercent, FiActivity } from 'react-icons/fi';
|
import { FiAward, FiPercent, FiActivity } from 'react-icons/fi';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
function getStatusColor(validCert) {
|
function getStatusColor(validCert) {
|
||||||
if (validCert === false) return 'bg-red-600';
|
if (validCert === false) return 'bg-red-600';
|
||||||
@@ -38,19 +39,31 @@ function getAvgPingBg(avgPing) {
|
|||||||
|
|
||||||
function getUptime24hBg(uptime) {
|
function getUptime24hBg(uptime) {
|
||||||
if (typeof uptime !== 'number') return 'bg-gray-300 dark:bg-gray-700';
|
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-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 >= 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';
|
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() {
|
export default function UptimeStatusIsland() {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
// Track open badge for each monitor by monitor.id and badge type
|
|
||||||
const [openBadge, setOpenBadge] = useState({});
|
const [openBadge, setOpenBadge] = useState({});
|
||||||
// Refs for each badge popup
|
|
||||||
const badgeRefs = useRef({});
|
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 () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -68,7 +81,9 @@ export default function UptimeStatusIsland() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData(); // initial fetch
|
||||||
|
const interval = setInterval(fetchData, 300000); // fetch every 5 minutes
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
// Helper to toggle badge info
|
// Helper to toggle badge info
|
||||||
@@ -106,6 +121,11 @@ export default function UptimeStatusIsland() {
|
|||||||
return badgeRefs.current[monitorId][badge];
|
return badgeRefs.current[monitorId][badge];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only render cards when timezone and locale are available
|
||||||
|
if (!userZone || !userLocale) {
|
||||||
|
return <div className="text-center text-gray-500 dark:text-gray-400">Loading timezone...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="grid gap-10 md:grid-cols-2">
|
<div className="grid gap-10 md:grid-cols-2">
|
||||||
@@ -161,27 +181,28 @@ export default function UptimeStatusIsland() {
|
|||||||
{monitor.avgPing !== undefined && (
|
{monitor.avgPing !== undefined && (
|
||||||
<>
|
<>
|
||||||
{/* Desktop: full badge */}
|
{/* Desktop: full badge */}
|
||||||
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}>
|
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}
|
||||||
Avg.: {monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms
|
title={`Avg. response: ${monitor.avgPing} ms`}>
|
||||||
|
Avg: {monitor.avgPing} ms
|
||||||
</span>
|
</span>
|
||||||
{/* Mobile: icon badge with click */}
|
{/* Mobile: icon badge with click */}
|
||||||
<span
|
<span
|
||||||
ref={getBadgeRef(monitor.id, 'avg')}
|
ref={getBadgeRef(monitor.id, 'avgPing')}
|
||||||
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
||||||
onClick={() => toggleBadge(monitor.id, 'avg')}
|
onClick={() => toggleBadge(monitor.id, 'avgPing')}
|
||||||
style={{ cursor: 'pointer', position: 'relative' }}
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
>
|
>
|
||||||
<FiActivity className="w-4 h-4" />
|
<FiActivity className="w-4 h-4" />
|
||||||
{openBadge[monitor.id]?.avg && (
|
{openBadge[monitor.id]?.avgPing && (
|
||||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-20 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap border border-gray-200 dark:border-gray-700">
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-20 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap border border-gray-200 dark:border-gray-700">
|
||||||
{monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms
|
{monitor.avgPing} ms
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 24h (right, col-span-1) */}
|
{/* 24h Uptime (right, col-span-1) */}
|
||||||
<div className="flex flex-col items-center col-span-1 relative">
|
<div className="flex flex-col items-center col-span-1 relative">
|
||||||
{monitor.uptime24h !== undefined && (
|
{monitor.uptime24h !== undefined && (
|
||||||
<>
|
<>
|
||||||
@@ -214,10 +235,11 @@ export default function UptimeStatusIsland() {
|
|||||||
{Array.from({ length: 40 }).map((_, i) => {
|
{Array.from({ length: 40 }).map((_, i) => {
|
||||||
const hbArr = monitor.heartbeatHistory ?? [];
|
const hbArr = monitor.heartbeatHistory ?? [];
|
||||||
const hb = hbArr.slice(-40)[i];
|
const hb = hbArr.slice(-40)[i];
|
||||||
|
const localTime = hb ? formatLocalTime(hb.time) : '';
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
title={hb ? `Status: ${hb.status === 1 ? 'Up' : 'Down'}\nTime: ${hb.time}` : ''}
|
title={hb ? `Status: ${hb.status === 1 ? 'Up' : 'Down'}\nTime: ${localTime}` : ''}
|
||||||
className={`w-full h-4 rounded-sm ${hb ? getHeartbeatColor(hb.status) : 'bg-gray-400 dark:bg-gray-600'} block`}
|
className={`w-full h-4 rounded-sm ${hb ? getHeartbeatColor(hb.status) : 'bg-gray-400 dark:bg-gray-600'} block`}
|
||||||
></span>
|
></span>
|
||||||
);
|
);
|
||||||
|
@@ -7,7 +7,7 @@ const STATUS_PAGE_SLUG = '365devnet'; // all lowercase
|
|||||||
|
|
||||||
interface Heartbeat {
|
interface Heartbeat {
|
||||||
status: number; // 0=DOWN, 1=UP, 2=PENDING, 3=MAINTENANCE
|
status: number; // 0=DOWN, 1=UP, 2=PENDING, 3=MAINTENANCE
|
||||||
time: string;
|
time: string; // UTC ISO string
|
||||||
msg: string;
|
msg: string;
|
||||||
ping: number | null;
|
ping: number | null;
|
||||||
important?: boolean;
|
important?: boolean;
|
||||||
@@ -28,7 +28,7 @@ interface Monitor {
|
|||||||
uptimePercent?: number;
|
uptimePercent?: number;
|
||||||
currentPing?: number;
|
currentPing?: number;
|
||||||
avgPing?: number;
|
avgPing?: number;
|
||||||
lastChecked?: string;
|
lastChecked?: string; // UTC ISO string
|
||||||
uptime24h?: number;
|
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 () => {
|
export const GET: APIRoute = async () => {
|
||||||
try {
|
try {
|
||||||
if (!UPTIME_KUMA_URL) {
|
if (!UPTIME_KUMA_URL) {
|
||||||
@@ -107,7 +113,11 @@ export const GET: APIRoute = async () => {
|
|||||||
for (const group of data.publicGroupList) {
|
for (const group of data.publicGroupList) {
|
||||||
for (const monitor of group.monitorList) {
|
for (const monitor of group.monitorList) {
|
||||||
const hbArr = heartbeatList[monitor.id.toString()] || [];
|
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)
|
// Uptime % (last 40 heartbeats)
|
||||||
if (hbArr.length > 0) {
|
if (hbArr.length > 0) {
|
||||||
const last40 = hbArr.slice(-40);
|
const last40 = hbArr.slice(-40);
|
||||||
@@ -118,8 +128,8 @@ export const GET: APIRoute = async () => {
|
|||||||
// Average ping (last 40 heartbeats)
|
// Average ping (last 40 heartbeats)
|
||||||
const pings = last40.map(hb => hb.ping).filter(p => typeof p === 'number') as number[];
|
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;
|
monitor.avgPing = pings.length > 0 ? Math.round((pings.reduce((a, b) => a + b, 0) / pings.length) * 10) / 10 : undefined;
|
||||||
// Last checked
|
// Last checked (ensure UTC)
|
||||||
monitor.lastChecked = last40[last40.length - 1]?.time ?? undefined;
|
monitor.lastChecked = last40[last40.length - 1]?.time ? ensureUTC(last40[last40.length - 1].time) : undefined;
|
||||||
} else {
|
} else {
|
||||||
monitor.uptimePercent = undefined;
|
monitor.uptimePercent = undefined;
|
||||||
monitor.currentPing = undefined;
|
monitor.currentPing = undefined;
|
||||||
|
@@ -1,14 +1,46 @@
|
|||||||
import { I18N } from 'astrowind:config';
|
import { I18N } from 'astrowind:config';
|
||||||
|
|
||||||
|
// Format dates in user's local timezone
|
||||||
export const formatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(I18N?.language, {
|
export const formatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(I18N?.language, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
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 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) => {
|
export const trim = (str = '', ch?: string) => {
|
||||||
let start = 0,
|
let start = 0,
|
||||||
end = str.length || 0;
|
end = str.length || 0;
|
||||||
|
Reference in New Issue
Block a user