Enhance ToggleTheme component and add Uptime link in Footer with translations
- Updated ToggleTheme component styles for better visibility in dark mode. - Added a new Uptime link in the Footer for system status monitoring. - Introduced translations for Uptime in English, Dutch, German, and French to support multilingual users.
This commit is contained in:
43
src/components/UpdateTimer.jsx
Normal file
43
src/components/UpdateTimer.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const REFRESH_INTERVAL = 300; // seconds
|
||||
|
||||
export default function UpdateTimer({ onRefresh }) {
|
||||
const [secondsLeft, setSecondsLeft] = useState(REFRESH_INTERVAL);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setSecondsLeft((prev) => {
|
||||
if (prev > 1) {
|
||||
return prev - 1;
|
||||
} else {
|
||||
if (typeof onRefresh === 'function') onRefresh();
|
||||
return REFRESH_INTERVAL;
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [onRefresh]);
|
||||
|
||||
const percent = (secondsLeft / REFRESH_INTERVAL) * 100;
|
||||
const coverPercent = 100 - percent;
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center mt-2 mb-2 max-w-5xl mx-auto">
|
||||
<div className="flex justify-center items-center mb-0.5 w-full">
|
||||
<span className="text-base font-semibold text-gray-300 dark:text-gray-200 text-center w-full">Next update in {secondsLeft}s</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-3 rounded bg-gray-300 dark:bg-gray-700 overflow-hidden relative"
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #22c55e 0%, #eab308 60%, #ef4444 100%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-3 bg-gray-300 dark:bg-gray-700 absolute top-0 right-0 transition-all duration-300"
|
||||
style={{ width: `${coverPercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
15
src/components/UptimeStatus.astro
Normal file
15
src/components/UptimeStatus.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import UptimeStatusIsland from './UptimeStatusIsland.jsx';
|
||||
import UpdateTimer from './UpdateTimer.jsx';
|
||||
---
|
||||
<UpdateTimer client:only="react" onRefresh={() => {}} />
|
||||
<UptimeStatusIsland client:only="react" />
|
||||
|
||||
|
||||
<style>
|
||||
/**** Custom grid for 40 heartbeat squares ****/
|
||||
.grid-cols-40 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(40, minmax(0, 1fr));
|
||||
}
|
||||
</style>
|
151
src/components/UptimeStatusIsland.jsx
Normal file
151
src/components/UptimeStatusIsland.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
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-red-600'; // DOWN
|
||||
if (status === 2) return 'bg-yellow-400'; // PENDING
|
||||
if (status === 3) return 'bg-blue-500'; // MAINTENANCE
|
||||
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.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';
|
||||
return 'bg-red-600 text-white dark:bg-red-800 dark:text-white';
|
||||
}
|
||||
|
||||
export default function UptimeStatusIsland() {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
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);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid gap-10 md:grid-cols-2">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">Loading...</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-red-500 dark:text-red-400">{error}</div>
|
||||
) : data && data.publicGroupList && data.publicGroupList.length > 0 ? (
|
||||
data.publicGroupList.map((group) => (
|
||||
<section key={group.id} className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-8 flex flex-col">
|
||||
<h2 className="text-2xl font-bold mb-6 text-gray-900 dark:text-white">{group.name}</h2>
|
||||
<ul className="space-y-6">
|
||||
{group.monitorList.map((monitor) => (
|
||||
<li key={monitor.id} className="flex flex-col gap-2 bg-gray-50 dark:bg-slate-900 rounded-xl px-5 py-4 shadow border border-gray-200 dark:border-gray-700">
|
||||
{/* First row: 5-column grid for fixed badge placement, name gets col-span-2 */}
|
||||
<div className="grid grid-cols-5 items-center w-full gap-2">
|
||||
{/* Name (left, col-span-2, min-w-0) */}
|
||||
<div className="flex items-center min-w-0 col-span-2">
|
||||
<span className={`inline-block w-3 h-3 rounded-full ${getStatusColor(monitor.validCert)} mr-2`}></span>
|
||||
<span className="font-semibold text-lg text-gray-800 dark:text-gray-100 truncate">{monitor.name}</span>
|
||||
</div>
|
||||
{/* Cert Exp (center-left, col-span-1) */}
|
||||
<div className="flex justify-center col-span-1">
|
||||
{monitor.certExpiryDaysRemaining !== undefined && (
|
||||
<span className={`px-2 py-0 rounded-full text-xs font-medium text-white ${getCertBg(monitor.certExpiryDaysRemaining)}`}
|
||||
title={monitor.certExpiryDaysRemaining < 0 ? 'Certificate expired!' : `Certificate expires in ${monitor.certExpiryDaysRemaining} days`}>
|
||||
{getCertText(monitor.certExpiryDaysRemaining)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Avg. (center-right, col-span-1) */}
|
||||
<div className="flex justify-center col-span-1">
|
||||
{monitor.avgPing !== undefined && (
|
||||
<span className={`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>
|
||||
)}
|
||||
</div>
|
||||
{/* 24h (right, col-span-1) */}
|
||||
<div className="flex justify-end col-span-1">
|
||||
{monitor.uptime24h !== undefined && (
|
||||
<span className={`px-2 py-0 rounded-full text-xs font-medium ${getUptime24hBg(monitor.uptime24h)}`}>
|
||||
24h: {monitor.uptime24h.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Second row: Heartbeat bar, always full width, evenly distributed squares */}
|
||||
{monitor.heartbeatHistory && monitor.heartbeatHistory.length > 0 && (
|
||||
<div className="flex items-center w-full">
|
||||
<div className="grid grid-cols-40 gap-x-0.5 bg-gray-200 dark:bg-slate-700 rounded px-2 py-1 w-full overflow-x-auto">
|
||||
{Array.from({ length: 40 }).map((_, i) => {
|
||||
const hbArr = monitor.heartbeatHistory ?? [];
|
||||
const hb = hbArr.slice(-40)[i];
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
title={hb ? `Status: ${hb.status === 1 ? 'Up' : 'Down'}\nTime: ${hb.time}` : ''}
|
||||
className={`w-full h-4 rounded-sm ${hb ? getHeartbeatColor(hb.status) : 'bg-gray-400 dark:bg-gray-600'} block`}
|
||||
></span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">No status data available</div>
|
||||
)}
|
||||
</div>
|
||||
<style>{`
|
||||
.grid-cols-40 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(40, minmax(0, 1fr));
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -13,7 +13,7 @@ export interface Props {
|
||||
const {
|
||||
label = 'Toggle between Dark and Light mode',
|
||||
class:
|
||||
className = 'text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
|
||||
className = 'text-muted dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
|
||||
iconClass = 'w-6 h-6',
|
||||
iconName = 'tabler:sun',
|
||||
} = Astro.props;
|
||||
|
@@ -125,6 +125,12 @@ const {
|
||||
</a>
|
||||
))
|
||||
}
|
||||
<a
|
||||
class="hover:text-gray-700 hover:underline dark:hover:text-gray-200 transition duration-150 ease-in-out"
|
||||
href="/uptime"
|
||||
>
|
||||
Uptime
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -131,6 +131,10 @@ export interface Translation {
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
uptime: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const supportedLanguages = ['en', 'nl', 'de', 'fr'] as const;
|
||||
@@ -573,7 +577,11 @@ export const translations: Record<string, Translation> = {
|
||||
'Experienced in providing advanced technical support for complex IT issues that require in-depth knowledge and specialized expertise. Proficient in troubleshooting, diagnosing, and resolving critical system problems across various platforms and applications.',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
uptime: {
|
||||
title: 'System Status',
|
||||
subtitle: 'Real-time monitoring of our services and systems',
|
||||
},
|
||||
},
|
||||
nl: {
|
||||
metadata: {
|
||||
@@ -1010,6 +1018,10 @@ export const translations: Record<string, Translation> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
uptime: {
|
||||
title: 'Systeemstatus',
|
||||
subtitle: 'Realtime monitoring van onze diensten en systemen',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
metadata: {
|
||||
@@ -1115,8 +1127,8 @@ export const translations: Record<string, Translation> = {
|
||||
missionContent: [
|
||||
'Wir sind bestrebt, IT-Exzellenz durch strategische Cloud-Optimierung, Prozessautomatisierung und technischen Support auf Unternehmensebene voranzutreiben. Wir nutzen modernste Technologie, um komplexe geschäftliche Herausforderungen zu bewältigen und messbaren Mehrwert zu liefern.',
|
||||
'Mit tiefgreifender Expertise in Microsoft-Technologien und Automatisierung befähigen wir Organisationen, ihre digitalen Fähigkeiten zu transformieren und ihre Geschäftsziele zu erreichen. Wir entwickeln Lösungen, die die Benutzererfahrung verbessern und die Produktivität maximieren, damit Technologie Ihr Unternehmen stärkt.',
|
||||
'Wir bleiben an der Spitze durch Erforschung und Implementierung aufkommender Technologien und bieten skalierbare Lösungen, die sich an Ihre sich entwickelnden Anforderungen anpassen. Wir stimmen technische Lösungen auf Ihre Kerngeschäftsziele ab und liefern messbaren ROI und Wettbewerbsvorteile.',
|
||||
'Unsere Mission ist es, Technologie zu nutzen, um echte geschäftliche Herausforderungen zu lösen und durch Innovation Wert zu schaffen. Mit über 15 Jahren IT-Erfahrung bringen wir einen reichen Schatz an Wissen in Microsoft-Technologieën, automatiseringstools und systeemintegratie om organisaties te helpen ihre digitalen Fähigkeiten zu transformieren und ihre strategischen Ziele zu erreichen.',
|
||||
'Wir bleiben an der Spitze durch Erforschung und Implementierung aufkommender Technologien und bieten schaalbare Lösungen, die sich an Ihre sich entwickelnden Anforderungen anpassen. Wir stimmen technische Lösungen auf Ihre Kerngeschäftsziele ab und liefern messbaren ROI und Wettbewerbsvorteile.',
|
||||
'Unsere Mission ist es, Technologie zu nutzen, um echte geschäftliche Herausforderungen zu lösen und durch Innovation Wert zu schaffen. Mit über 15 Jahren IT-Erfahrung bringen wir eine reiche Schatz an Wissen in Microsoft-Technologieën, automatiseringstools und systeemintegratie um organisaties te helpen ihre digitalen Fähigkeiten zu transformieren und ihre strategischen Ziele zu erreichen.',
|
||||
],
|
||||
items: [
|
||||
{
|
||||
@@ -1446,6 +1458,10 @@ export const translations: Record<string, Translation> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
uptime: {
|
||||
title: 'Systemstatus',
|
||||
subtitle: 'Echtzeitüberwachung unserer Dienste und Systeme',
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
metadata: {
|
||||
@@ -1881,5 +1897,9 @@ export const translations: Record<string, Translation> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
uptime: {
|
||||
title: 'Statut du système',
|
||||
subtitle: 'Surveillance en temps réel de nos services et systèmes',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
47
src/pages/[lang]/uptime.astro
Normal file
47
src/pages/[lang]/uptime.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import UptimeStatus from '../../components/UptimeStatus.astro';
|
||||
import Layout from '../../layouts/PageLayout.astro';
|
||||
import { supportedLanguages, getTranslation } from '~/i18n/translations';
|
||||
|
||||
// Define the type for supported languages
|
||||
type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||
|
||||
// Get current language from URL
|
||||
const currentPath = `/${Astro.url.pathname.replace(/^\/+/g, '').replace(/\/+$/, '')}`;
|
||||
const pathSegments = currentPath.split('/').filter(Boolean);
|
||||
|
||||
// Check for language in URL path
|
||||
let currentLang =
|
||||
pathSegments[0] && supportedLanguages.includes(pathSegments[0] as SupportedLanguage)
|
||||
? (pathSegments[0] as SupportedLanguage)
|
||||
: null;
|
||||
|
||||
// If no language in URL, check cookies
|
||||
if (!currentLang) {
|
||||
const cookies = Astro.request.headers.get('cookie') || '';
|
||||
const cookieLanguage = cookies
|
||||
.split(';')
|
||||
.map((cookie) => cookie.trim())
|
||||
.find((cookie) => cookie.startsWith('preferredLanguage='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage)) {
|
||||
currentLang = cookieLanguage as SupportedLanguage;
|
||||
} else {
|
||||
// Default to English if no language is found
|
||||
currentLang = 'en';
|
||||
}
|
||||
}
|
||||
|
||||
const t = getTranslation(currentLang);
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 py-12">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">{t.uptime.title}</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300">{t.uptime.subtitle}</p>
|
||||
</div>
|
||||
<UptimeStatus />
|
||||
</main>
|
||||
</Layout>
|
156
src/pages/api/uptime.ts
Normal file
156
src/pages/api/uptime.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fetch from 'node-fetch';
|
||||
import type { RequestInit } from 'node-fetch';
|
||||
|
||||
const UPTIME_KUMA_URL = import.meta.env.UPTIME_KUMA_URL;
|
||||
const STATUS_PAGE_SLUG = '365devnet'; // all lowercase
|
||||
|
||||
interface Heartbeat {
|
||||
status: number; // 0=DOWN, 1=UP, 2=PENDING, 3=MAINTENANCE
|
||||
time: string;
|
||||
msg: string;
|
||||
ping: number | null;
|
||||
important?: boolean;
|
||||
duration?: number;
|
||||
localDateTime?: string;
|
||||
timezone?: string;
|
||||
retries?: number;
|
||||
downCount?: number;
|
||||
}
|
||||
|
||||
interface Monitor {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
certExpiryDaysRemaining?: number;
|
||||
validCert?: boolean;
|
||||
heartbeatHistory?: Heartbeat[];
|
||||
uptimePercent?: number;
|
||||
currentPing?: number;
|
||||
avgPing?: number;
|
||||
lastChecked?: string;
|
||||
uptime24h?: number;
|
||||
}
|
||||
|
||||
interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
monitorList: Monitor[];
|
||||
}
|
||||
|
||||
// Add timeout to fetch request
|
||||
const fetchWithTimeout = async (url: string, options: RequestInit, timeout = 10000) => {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(id);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(id);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
try {
|
||||
if (!UPTIME_KUMA_URL) {
|
||||
console.error('Missing environment variable: UPTIME_KUMA_URL');
|
||||
return new Response(JSON.stringify({ error: 'Configuration error: Missing environment variable' }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch main status page data
|
||||
const statusPageUrl = `${UPTIME_KUMA_URL}/api/status-page/${STATUS_PAGE_SLUG}`;
|
||||
const response = await fetchWithTimeout(
|
||||
statusPageUrl,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
10000 // 10 second timeout
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { publicGroupList?: Group[] };
|
||||
|
||||
// Fetch all heartbeat history in one call
|
||||
const heartbeatUrl = `${UPTIME_KUMA_URL}/api/status-page/heartbeat/${STATUS_PAGE_SLUG}`;
|
||||
const heartbeatResp = await fetchWithTimeout(
|
||||
heartbeatUrl,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
10000
|
||||
);
|
||||
const heartbeatData = (await heartbeatResp.json()) as { heartbeatList: Record<string, Heartbeat[]>; uptimeList?: Record<string, number> };
|
||||
const heartbeatList: Record<string, Heartbeat[]> = heartbeatData.heartbeatList || {};
|
||||
const uptimeList: Record<string, number> = heartbeatData.uptimeList || {};
|
||||
|
||||
// Attach heartbeat history and calculated stats to each monitor
|
||||
if (data && typeof data === 'object' && Array.isArray(data.publicGroupList)) {
|
||||
for (const group of data.publicGroupList) {
|
||||
for (const monitor of group.monitorList) {
|
||||
const hbArr = heartbeatList[monitor.id.toString()] || [];
|
||||
monitor.heartbeatHistory = hbArr;
|
||||
// Uptime % (last 40 heartbeats)
|
||||
if (hbArr.length > 0) {
|
||||
const last40 = hbArr.slice(-40);
|
||||
const upCount = last40.filter(hb => hb.status === 1).length;
|
||||
monitor.uptimePercent = Math.round((upCount / last40.length) * 1000) / 10; // 1 decimal
|
||||
// Current ping (most recent heartbeat)
|
||||
monitor.currentPing = last40[last40.length - 1]?.ping ?? undefined;
|
||||
// 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;
|
||||
} else {
|
||||
monitor.uptimePercent = undefined;
|
||||
monitor.currentPing = undefined;
|
||||
monitor.avgPing = undefined;
|
||||
monitor.lastChecked = undefined;
|
||||
}
|
||||
// Attach 24h uptime percentage if available
|
||||
const uptimeKey = `${monitor.id}_24`;
|
||||
if (uptimeList[uptimeKey] !== undefined) {
|
||||
monitor.uptime24h = Math.round(uptimeList[uptimeKey] * 10000) / 100; // e.g. 0.9998 -> 99.98
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching uptime data:', err);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to fetch uptime data',
|
||||
details: err instanceof Error ? err.message : 'Unknown error'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user