mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-17 15:36:50 +00:00
Uptime functionality
This commit is contained in:
parent
ed46598c27
commit
2f6957a45d
145
app/api/applications/uptime/route.ts
Normal file
145
app/api/applications/uptime/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,17 +14,57 @@ import {
|
|||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axios from "axios"; // Korrekter Import
|
import axios from "axios";
|
||||||
import { Card, CardHeader } from "@/components/ui/card";
|
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 {
|
const timeFormats = {
|
||||||
serverCount: number;
|
1: (timestamp: string) =>
|
||||||
applicationCount: number;
|
new Date(timestamp).toLocaleTimeString([], {
|
||||||
onlineApplicationsCount: number;
|
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() {
|
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 (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
@ -36,9 +76,7 @@ export default function Uptime() {
|
|||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem className="hidden md:block">
|
<BreadcrumbItem className="hidden md:block">
|
||||||
<BreadcrumbPage>
|
<BreadcrumbPage>/</BreadcrumbPage>
|
||||||
/
|
|
||||||
</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
@ -53,16 +91,105 @@ export default function Uptime() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="pl-4 pr-4">
|
<div className="pl-4 pr-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-2xl font-semibold">Uptime</span>
|
<span className="text-2xl font-semibold">Uptime</span>
|
||||||
<div className="pt-4">
|
<Select
|
||||||
<Card className="w-full relative">
|
value={String(timespan)}
|
||||||
<CardHeader>
|
onValueChange={(v) => setTimespan(Number(v) as 1 | 2 | 3)}
|
||||||
<div className="flex flex-col gap-4">
|
>
|
||||||
<span className="text-lg font-semibold">Application Name - Uptime</span>
|
<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>
|
</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>
|
</Card>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user