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:
becarta
2025-06-08 02:43:05 +02:00
parent 8e0e26c50b
commit a084480695
5 changed files with 132 additions and 56 deletions

View File

@@ -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 <div className="text-center text-gray-500 dark:text-gray-400">Loading timezone...</div>;
}
return (
<div>
<div className="grid gap-10 md:grid-cols-2">
@@ -161,27 +181,28 @@ export default function UptimeStatusIsland() {
{monitor.avgPing !== undefined && (
<>
{/* Desktop: full badge */}
<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
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}
title={`Avg. response: ${monitor.avgPing} ms`}>
Avg: {monitor.avgPing} ms
</span>
{/* Mobile: icon badge with click */}
<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)}`}
onClick={() => toggleBadge(monitor.id, 'avg')}
onClick={() => toggleBadge(monitor.id, 'avgPing')}
style={{ cursor: 'pointer', position: 'relative' }}
>
<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">
{monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms
{monitor.avgPing} ms
</span>
)}
</span>
</>
)}
</div>
{/* 24h (right, col-span-1) */}
{/* 24h Uptime (right, col-span-1) */}
<div className="flex flex-col items-center col-span-1 relative">
{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 (
<span
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`}
></span>
);

View File

@@ -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;

View File

@@ -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;