Uptime functionality

This commit is contained in:
headlessdev 2025-04-15 13:46:25 +02:00
parent ed46598c27
commit 2f6957a45d
2 changed files with 289 additions and 17 deletions

View File

@ -0,0 +1,145 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface RequestBody {
timespan?: number;
}
const getTimeRange = (timespan: number) => {
const now = new Date();
switch (timespan) {
case 1: // 30 Minuten
return {
start: new Date(now.getTime() - 30 * 60 * 1000),
interval: 'minute'
};
case 2: // 7 Tage
return {
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
interval: '3hour'
};
case 3: // 30 Tage
return {
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
interval: 'day'
};
default:
return {
start: new Date(now.getTime() - 30 * 60 * 1000),
interval: 'minute'
};
}
};
const generateIntervals = (timespan: number) => {
const now = new Date();
now.setSeconds(0, 0);
switch (timespan) {
case 1: // 30 Minuten
return Array.from({ length: 30 }, (_, i) => {
const d = new Date(now);
d.setMinutes(d.getMinutes() - i);
d.setSeconds(0, 0);
return d;
});
case 2: // 7 Tage (56 Intervalle à 3 Stunden)
return Array.from({ length: 56 }, (_, i) => {
const d = new Date(now);
d.setHours(d.getHours() - (i * 3));
d.setMinutes(0, 0, 0);
return d;
});
case 3: // 30 Tage
return Array.from({ length: 30 }, (_, i) => {
const d = new Date(now);
d.setDate(d.getDate() - i);
d.setHours(0, 0, 0, 0);
return d;
});
default:
return [];
}
};
const getIntervalKey = (date: Date, timespan: number) => {
const d = new Date(date);
switch (timespan) {
case 1:
d.setSeconds(0, 0);
return d.toISOString();
case 2:
d.setHours(Math.floor(d.getHours() / 3) * 3);
d.setMinutes(0, 0, 0);
return d.toISOString();
case 3:
d.setHours(0, 0, 0, 0);
return d.toISOString();
default:
return d.toISOString();
}
};
export async function POST(request: NextRequest) {
try {
const { timespan = 1 }: RequestBody = await request.json();
const { start } = getTimeRange(timespan);
const applications = await prisma.application.findMany();
const uptimeHistory = await prisma.uptime_history.findMany({
where: {
applicationId: { in: applications.map(app => app.id) },
createdAt: { gte: start }
},
orderBy: { createdAt: "desc" }
});
const intervals = generateIntervals(timespan);
const result = applications.map(app => {
const appChecks = uptimeHistory.filter(check => check.applicationId === app.id);
const checksMap = new Map<string, { failed: number; total: number }>();
for (const check of appChecks) {
const intervalKey = getIntervalKey(check.createdAt, timespan);
const current = checksMap.get(intervalKey) || { failed: 0, total: 0 };
current.total++;
if (!check.online) current.failed++;
checksMap.set(intervalKey, current);
}
const uptimeSummary = intervals.map(interval => {
const intervalKey = getIntervalKey(interval, timespan);
const stats = checksMap.get(intervalKey);
if (!stats) {
return {
timestamp: interval.toISOString(),
missing: true,
online: null
};
}
return {
timestamp: intervalKey,
missing: false,
online: stats.failed < 3
};
});
return {
appName: app.name,
appId: app.id,
uptimeSummary
};
});
return NextResponse.json(result);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@ -14,17 +14,57 @@ import {
SidebarTrigger,
} from "@/components/ui/sidebar";
import { useEffect, useState } from "react";
import axios from "axios"; // Korrekter Import
import axios from "axios";
import { Card, CardHeader } from "@/components/ui/card";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface StatsResponse {
serverCount: number;
applicationCount: number;
onlineApplicationsCount: number;
}
const timeFormats = {
1: (timestamp: string) =>
new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false
}),
2: (timestamp: string) => {
const start = new Date(timestamp);
const end = new Date(start.getTime() + 3 * 60 * 60 * 1000);
return `${start.toLocaleDateString([], { day: '2-digit', month: 'short' })}
${start.getHours().toString().padStart(2, '0')}:00 -
${end.getHours().toString().padStart(2, '0')}:00`;
},
3: (timestamp: string) =>
new Date(timestamp).toLocaleDateString([], {
day: '2-digit',
month: 'short'
})
};
const gridColumns = {
1: 30,
2: 56,
3: 30
};
export default function Uptime() {
const [data, setData] = useState<any[]>([]);
const [timespan, setTimespan] = useState<1 | 2 | 3>(1);
const getData = async (selectedTimespan: number) => {
try {
const response = await axios.post("/api/applications/uptime", {
timespan: selectedTimespan
});
setData(response.data);
} catch (error) {
console.error("Error fetching data:", error);
}
};
useEffect(() => {
getData(timespan);
}, [timespan]);
return (
<SidebarProvider>
<AppSidebar />
@ -36,9 +76,7 @@ export default function Uptime() {
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>
/
</BreadcrumbPage>
<BreadcrumbPage>/</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
@ -53,16 +91,105 @@ export default function Uptime() {
</div>
</header>
<div className="pl-4 pr-4">
<div className="flex justify-between items-center">
<span className="text-2xl font-semibold">Uptime</span>
<div className="pt-4">
<Card className="w-full relative">
<CardHeader>
<div className="flex flex-col gap-4">
<span className="text-lg font-semibold">Application Name - Uptime</span>
<Select
value={String(timespan)}
onValueChange={(v) => setTimespan(Number(v) as 1 | 2 | 3)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select timespan" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Last 30 minutes</SelectItem>
<SelectItem value="2">Last 7 days</SelectItem>
<SelectItem value="3">Last 30 days</SelectItem>
</SelectContent>
</Select>
</div>
<div className="pt-4 space-y-4 pb-4">
{data.map((app) => {
const reversedSummary = [...app.uptimeSummary].reverse();
const startTime = reversedSummary[0]?.timestamp;
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
return (
<Card key={app.appId}>
<CardHeader>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">{app.appName}</span>
</div>
<div className="flex flex-col gap-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>{startTime ? timeFormats[timespan](startTime) : ""}</span>
<span>{endTime ? timeFormats[timespan](endTime) : ""}</span>
</div>
</CardHeader>
<Tooltip.Provider>
<div
className="grid gap-0.5 w-full overflow-x-auto pb-2"
style={{
gridTemplateColumns: `repeat(${gridColumns[timespan]}, minmax(0, 1fr))`,
minWidth: `${gridColumns[timespan] * 24}px`
}}
>
{reversedSummary.map((entry) => (
<Tooltip.Root key={entry.timestamp}>
<Tooltip.Trigger asChild>
<div
className={`h-8 w-full rounded-sm border transition-colors ${
entry.missing
? "bg-gray-300 border-gray-400"
: entry.online
? "bg-green-500 border-green-600"
: "bg-red-500 border-red-600"
}`}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="rounded bg-gray-900 px-2 py-1 text-white text-xs shadow-lg"
side="top"
>
<div className="flex flex-col gap-1">
<p className="font-medium">
{timespan === 2 ? (
timeFormats[2](entry.timestamp)
) : (
new Date(entry.timestamp).toLocaleString([], {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: timespan === 3 ? undefined : '2-digit',
hour12: false
})
)}
</p>
<p>
{entry.missing
? "No data"
: entry.online
? "Online"
: "Offline"}
</p>
</div>
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
))}
</div>
</Tooltip.Provider>
</div>
</div>
</CardHeader>
</Card>
</div>
);
})}
</div>
</div>
</SidebarInset>
</SidebarProvider>