From 2f6957a45db96ac4cad1ddb27ffd1cd28eb0a0f1 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:46:25 +0200 Subject: [PATCH] Uptime functionality --- app/api/applications/uptime/route.ts | 145 ++++++++++++++++++++++++ app/dashboard/uptime/Uptime.tsx | 161 ++++++++++++++++++++++++--- 2 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 app/api/applications/uptime/route.ts diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts new file mode 100644 index 0000000..00950b3 --- /dev/null +++ b/app/api/applications/uptime/route.ts @@ -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(); + + 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 }); + } +} \ No newline at end of file diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 96d5739..21e37d3 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -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([]); + 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 ( @@ -36,9 +76,7 @@ export default function Uptime() { - - / - + / @@ -53,16 +91,105 @@ export default function Uptime() {
+
Uptime -
- - -
- Application Name - Uptime + +
+
+ {data.map((app) => { + const reversedSummary = [...app.uptimeSummary].reverse(); + const startTime = reversedSummary[0]?.timestamp; + const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; + + return ( + + +
+
+ {app.appName} +
+ +
+
+ {startTime ? timeFormats[timespan](startTime) : ""} + {endTime ? timeFormats[timespan](endTime) : ""}
- + + +
+ {reversedSummary.map((entry) => ( + + +
+ + + +
+

+ {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 + }) + )} +

+

+ {entry.missing + ? "No data" + : entry.online + ? "Online" + : "Offline"} +

+
+ +
+
+ + ))} +
+ +
+
+ -
+ ); + })} +