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:
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