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:
becarta
2025-06-08 01:13:56 +02:00
parent 7f06a0d546
commit 46fa503dda
8 changed files with 442 additions and 4 deletions

View 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
View 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',
},
});
}
};