v0.0.4 dev->main

v0.0.4
This commit is contained in:
headlessdev 2025-04-16 14:17:20 +02:00 committed by GitHub
commit 676354f53c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 2135 additions and 275 deletions

View File

@ -17,25 +17,30 @@ The only dashboard you'll ever need to manage your entire server infrastructure.
## Screenshots
Login Page:
![Login Page](https://i.ibb.co/QvvJvHxY/image.png)
![Login Page](https://i.ibb.co/DfS7BJdX/image.png)
Dashboard Page:
![Dashboard Page](https://i.ibb.co/G3FW5mVX/image.png)
![Dashboard Page](https://i.ibb.co/m5xMXz73/image.png)
Servers Page:
![Servers Page](https://i.ibb.co/v6Z79wJY/image.png)
![Servers Page](https://i.ibb.co/QFrFRp1B/image.png)
Applications Page:
![Applications Page](https://i.ibb.co/zC1f6s9/image.png)
![Applications Page](https://i.ibb.co/1JK3pFYG/image.png)
Uptime Page:
![Uptime Page](https://i.ibb.co/99LTnZ14/image.png)
Network Page:
![Network Page](https://i.ibb.co/XkKYrGQX/image.png)
![Network Page](https://i.ibb.co/1Y6ypKHk/image.png)
Settings Page:
![Settings Page](https://i.ibb.co/mrdjqy7f/image.png)
## Roadmap
- [ ] Edit Applications, Applications searchbar
- [ ] Customizable Dashboard
- [X] Edit Applications, Applications searchbar
- [X] Uptime History
- [ ] Notifications
- [ ] Uptime History
- [ ] Simple Server Monitoring
- [ ] Improved Network Flowchart with custom elements (like Network switches)
- [ ] Advanced Settings (Disable Uptime Tracking & more)
@ -50,10 +55,7 @@ services:
ports:
- "3000:3000"
environment:
LOGIN_EMAIL: "mail@example.com"
LOGIN_PASSWORD: "SecretPassword"
JWT_SECRET: RANDOM_SECRET
ACCOUNT_SECRET: RANDOM_SECRET
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
- db
@ -78,6 +80,10 @@ volumes:
postgres_data:
```
#### Default Login
__E-Mail:__ admin@example.com\
__Password:__ admin
## Tech Stack & Credits
The application is build with:
@ -87,6 +93,7 @@ The application is build with:
- PostgreSQL with [Prisma ORM](https://www.prisma.io/)
- Icons by [Lucide](https://lucide.dev/)
- Flowcharts by [React Flow](https://reactflow.dev/)
- Application icons by [selfh.st/icons](selfh.st/icons)
- and a lot of love ❤️
## Star History

View File

@ -1 +1 @@
.env.local
.env

View File

@ -34,19 +34,48 @@ func main() {
}
defer db.Close()
ticker := time.NewTicker(5 * time.Second)
go func() {
deletionTicker := time.NewTicker(1 * time.Hour)
defer deletionTicker.Stop()
for range deletionTicker.C {
if err := deleteOldEntries(db); err != nil {
fmt.Printf("Error deleting old entries: %v\n", err)
}
}
}()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
client := &http.Client{
Timeout: 4 * time.Second,
}
for range ticker.C {
for now := range ticker.C {
if now.Second()%10 != 0 {
continue
}
apps := getApplications(db)
checkAndUpdateStatus(db, client, apps)
}
}
func deleteOldEntries(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := db.ExecContext(ctx,
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
return nil
}
func getApplications(db *sql.DB) []Application {
rows, err := db.Query(`
SELECT id, "publicURL", online
@ -90,12 +119,21 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
}
_, err = db.ExecContext(ctx,
"UPDATE application SET online = $1 WHERE id = $2",
`UPDATE application SET online = $1 WHERE id = $2`,
isOnline,
app.ID,
)
if err != nil {
fmt.Printf("Update failed for app %d: %v\n", app.ID, err)
}
_, err = db.ExecContext(ctx,
`INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
app.ID,
isOnline,
)
if err != nil {
fmt.Printf("Insert into uptime_history failed for app %d: %v\n", app.ID, err)
}
}
}

View File

@ -14,6 +14,10 @@ export async function POST(request: NextRequest) {
where: { id: id }
});
await prisma.uptime_history.deleteMany({
where: { applicationId: id }
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });

View File

@ -0,0 +1,162 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface RequestBody {
timespan?: number;
page?: number;
}
const getTimeRange = (timespan: number) => {
const now = new Date();
switch (timespan) {
case 1:
return {
start: new Date(now.getTime() - 30 * 60 * 1000),
interval: 'minute'
};
case 2:
return {
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
interval: '3hour'
};
case 3:
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:
return Array.from({ length: 30 }, (_, i) => {
const d = new Date(now);
d.setMinutes(d.getMinutes() - i);
d.setSeconds(0, 0);
return d;
});
case 2:
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:
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, page = 1 }: RequestBody = await request.json();
const itemsPerPage = 5;
const skip = (page - 1) * itemsPerPage;
// Get paginated and sorted applications
const [applications, totalCount] = await Promise.all([
prisma.application.findMany({
skip,
take: itemsPerPage,
orderBy: { name: 'asc' }
}),
prisma.application.count()
]);
const applicationIds = applications.map(app => app.id);
// Get time range and intervals
const { start } = getTimeRange(timespan);
const intervals = generateIntervals(timespan);
// Get uptime history for the filtered applications
const uptimeHistory = await prisma.uptime_history.findMany({
where: {
applicationId: { in: applicationIds },
createdAt: { gte: start }
},
orderBy: { createdAt: "desc" }
});
// Process data for each application
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);
return {
timestamp: intervalKey,
missing: !stats,
online: stats ? (stats.failed / stats.total) <= 0.5 : null
};
});
return {
appName: app.name,
appId: app.id,
uptimeSummary
};
});
return NextResponse.json({
data: result,
pagination: {
currentPage: page,
totalPages: Math.ceil(totalCount / itemsPerPage),
totalItems: totalCount
}
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@ -0,0 +1,56 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
interface EditEmailRequest {
newEmail: string;
jwtToken: string;
}
export async function POST(request: NextRequest) {
try {
const body: EditEmailRequest = await request.json();
const { newEmail, jwtToken } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
// Verify JWT
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
if (!decoded.account_secret) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
// Get the user by account id
const user = await prisma.user.findUnique({
where: { id: decoded.account_secret },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Check if the new email is already in use
const existingUser = await prisma.user.findUnique({
where: { email: newEmail },
});
if (existingUser) {
return NextResponse.json({ error: 'Email already in use' }, { status: 400 });
}
// Update the user's email
await prisma.user.update({
where: { id: user.id },
data: { email: newEmail },
});
return NextResponse.json({ message: 'Email updated successfully' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,55 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
import bcrypt from 'bcrypt';
interface EditEmailRequest {
oldPassword: string;
newPassword: string;
jwtToken: string;
}
export async function POST(request: NextRequest) {
try {
const body: EditEmailRequest = await request.json();
const { oldPassword, newPassword, jwtToken } = body;
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
// Verify JWT
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
if (!decoded.account_secret) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
// Get the user by account id
const user = await prisma.user.findUnique({
where: { id: decoded.account_secret },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Check if the old password is correct
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
if (!isOldPasswordValid) {
return NextResponse.json({ error: 'Old password is incorrect' }, { status: 401 });
}
// Hash the new password
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
// Update the user's password
await prisma.user.update({
where: { id: user.id },
data: { password: hashedNewPassword },
});
return NextResponse.json({ message: 'Password updated successfully' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,5 +1,7 @@
import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
import bcrypt from 'bcrypt';
interface LoginRequest {
username: string;
@ -11,17 +13,50 @@ export async function POST(request: NextRequest) {
const body: LoginRequest = await request.json();
const { username, password } = body;
if(username !== process.env.LOGIN_EMAIL || password !== process.env.LOGIN_PASSWORD) {
throw new Error('Invalid credentials');
}
// Ensure JWT_SECRET is defined
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}
let accountId: string = '';
// Check if there are any entries in user
const userCount = await prisma.user.count();
if (userCount === 0) {
if(username=== "admin@example.com" && password === "admin") {
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);
// Create the first user with hashed password
const user = await prisma.user.create({
data: {
email: username,
password: hashedPassword,
},
});
// Get the account id
accountId = user.id;
} else {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
} else {
// Get the user by username
const user = await prisma.user.findUnique({
where: { email: username },
});
if (!user) {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
// Check if the password is correct
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
}
// Get the account id
accountId = user.id;
}
// Create JWT
const token = jwt.sign({ account_secret: process.env.ACCOUNT_SECRET }, process.env.JWT_SECRET, { expiresIn: '7d' });
const token = jwt.sign({ account_secret: accountId }, process.env.JWT_SECRET, { expiresIn: '7d' });
return NextResponse.json({ token });
} catch (error: any) {

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { prisma } from "@/lib/prisma";
interface ValidateRequest {
token: string;
@ -16,6 +16,14 @@ export async function POST(request: NextRequest) {
throw new Error('JWT_SECRET is not defined');
}
// Get the account id
const user = await prisma.user.findFirst({
where: {},
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Verify JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload & { id: string };
@ -23,7 +31,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}
if(decoded.account_secret !== process.env.ACCOUNT_SECRET) {
if(decoded.account_secret !== user.id) {
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
}

View File

@ -10,6 +10,9 @@ interface Node {
};
position: { x: number; y: number };
style: React.CSSProperties;
draggable?: boolean;
selectable?: boolean;
zIndex?: number;
}
interface Edge {
@ -38,10 +41,13 @@ interface Application {
const NODE_WIDTH = 220;
const NODE_HEIGHT = 60;
const APP_NODE_WIDTH = 160;
const APP_NODE_HEIGHT = 40;
const HORIZONTAL_SPACING = 280;
const VERTICAL_SPACING = 80;
const VERTICAL_SPACING = 60;
const START_Y = 120;
const ROOT_NODE_WIDTH = 300;
const CONTAINER_PADDING = 40;
export async function GET() {
try {
@ -54,11 +60,12 @@ export async function GET() {
}) as Promise<Application[]>,
]);
// Root Node
const rootNode: Node = {
id: "root",
type: "infrastructure",
data: { label: "My Infrastructure" },
position: { x: 0, y: 20 },
position: { x: 0, y: 0 },
style: {
background: "#ffffff",
color: "#0f0f0f",
@ -72,6 +79,7 @@ export async function GET() {
},
};
// Server Nodes
const serverNodes: Node[] = servers.map((server, index) => {
const xPos =
index * HORIZONTAL_SPACING -
@ -100,11 +108,12 @@ export async function GET() {
};
});
// Application Nodes
const appNodes: Node[] = [];
servers.forEach((server) => {
const serverX =
serverNodes.find((n) => n.id === `server-${server.id}`)?.position.x || 0;
const serverY = START_Y;
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
const serverX = serverNode?.position.x || 0;
const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2;
applications
.filter((app) => app.serverId === server.id)
@ -117,25 +126,26 @@ export async function GET() {
...app,
},
position: {
x: serverX,
y: serverY + NODE_HEIGHT + 40 + appIndex * VERTICAL_SPACING,
x: serverX + xOffset,
y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING,
},
style: {
background: "#ffffff",
background: "#f5f5f5",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "4px",
padding: "8px",
width: NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "0.9rem",
lineHeight: "1.2",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
});
});
// Connections
const connections: Edge[] = [
...servers.map((server) => ({
id: `conn-root-${server.id}`,
@ -159,8 +169,46 @@ export async function GET() {
})),
];
// Container Box
const allNodes = [rootNode, ...serverNodes, ...appNodes];
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
allNodes.forEach((node) => {
const width = parseInt(node.style.width?.toString() || "0", 10);
const height = parseInt(node.style.height?.toString() || "0", 10);
minX = Math.min(minX, node.position.x);
maxX = Math.max(maxX, node.position.x + width);
minY = Math.min(minY, node.position.y);
maxY = Math.max(maxY, node.position.y + height);
});
const containerNode: Node = {
id: 'container',
type: 'container',
data: { label: '' },
position: {
x: minX - CONTAINER_PADDING,
y: minY - CONTAINER_PADDING
},
style: {
width: maxX - minX + 2 * CONTAINER_PADDING,
height: maxY - minY + 2 * CONTAINER_PADDING,
background: 'transparent',
border: '2px dashed #e2e8f0',
borderRadius: '8px',
zIndex: 0,
},
draggable: false,
selectable: false,
zIndex: -1,
};
return NextResponse.json({
nodes: [rootNode, ...serverNodes, ...appNodes],
nodes: [containerNode, ...allNodes],
edges: connections,
});
} catch (error: unknown) {

View File

@ -9,6 +9,14 @@ export async function POST(request: NextRequest) {
if (!id) {
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
}
// Check if there are any applications associated with the server
const applications = await prisma.application.findMany({
where: { serverId: id }
});
if (applications.length > 0) {
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
}
await prisma.server.delete({
where: { id: id }

View File

@ -2,15 +2,16 @@ import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface GetRequest {
page: number;
page?: number;
ITEMS_PER_PAGE?: number;
}
const ITEMS_PER_PAGE = 5;
export async function POST(request: NextRequest) {
try {
const body: GetRequest = await request.json();
const page = Math.max(1, body.page || 1);
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
const servers = await prisma.server.findMany({
skip: (page - 1) * ITEMS_PER_PAGE,

View File

@ -1,62 +1,61 @@
import { AppSidebar } from "@/components/app-sidebar";
"use client"
import { useEffect, useState } from "react"
import axios from "axios"
import Link from "next/link"
import { Activity, Layers, Network, Server } from "lucide-react"
import { AppSidebar } from "@/components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { useEffect, useState } from "react";
import axios from "axios"; // Korrekter Import
import { Card, CardHeader } from "@/components/ui/card";
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
interface StatsResponse {
serverCount: number;
applicationCount: number;
onlineApplicationsCount: number;
serverCount: number
applicationCount: number
onlineApplicationsCount: number
}
export default function Dashboard() {
const [serverCount, setServerCount] = useState<number>(0);
const [applicationCount, setApplicationCount] = useState<number>(0);
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0);
const [serverCount, setServerCount] = useState<number>(0)
const [applicationCount, setApplicationCount] = useState<number>(0)
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
const getStats = async () => {
try {
const response = await axios.post<StatsResponse>('/api/dashboard/get', {});
setServerCount(response.data.serverCount);
setApplicationCount(response.data.applicationCount);
setOnlineApplicationsCount(response.data.onlineApplicationsCount);
const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
setServerCount(response.data.serverCount)
setApplicationCount(response.data.applicationCount)
setOnlineApplicationsCount(response.data.onlineApplicationsCount)
} catch (error: any) {
console.log("Axios error:", error.response?.data);
console.log("Axios error:", error.response?.data)
}
};
}
useEffect(() => {
getStats();
}, []);
getStats()
}, [])
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>
/
</BreadcrumbPage>
<BreadcrumbPage>/</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
@ -66,51 +65,105 @@ export default function Dashboard() {
</Breadcrumb>
</div>
</header>
<div className="pl-4 pr-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card className="w-full mb-4 relative">
<CardHeader>
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center">
<span className="text-2xl font-bold">{serverCount}</span>
<span className="text-md">Servers</span>
</div>
<div className="p-6">
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Servers</CardTitle>
<Server className="h-6 w-6 text-rose-500" />
</div>
<CardDescription>Manage your server infrastructure</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{serverCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active servers</p>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/servers">View all servers</Link>
</Button>
</CardFooter>
</Card>
<Card className="w-full mb-4 relative">
<CardHeader>
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center">
<span className="text-2xl font-bold">{applicationCount}</span>
<span className="text-md">Applications</span>
</div>
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Applications</CardTitle>
<Layers className="h-6 w-6 text-amber-500" />
</div>
<CardDescription>Manage your deployed applications</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/applications">View all applications</Link>
</Button>
</CardFooter>
</Card>
<Card className="w-full mb-4 relative">
<CardHeader>
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center">
<span className="text-2xl font-bold">
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Uptime</CardTitle>
<Activity className="h-6 w-6 text-emerald-500" />
</div>
<CardDescription>Monitor your service availability</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="flex flex-col">
<div className="text-4xl font-bold flex items-center justify-between">
<span>
{onlineApplicationsCount}/{applicationCount}
</span>
<span className="text-md">Applications are online</span>
<div className="flex items-center bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-lg font-semibold">
{applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-3">
<div
className="bg-emerald-500 h-2.5 rounded-full"
style={{
width: `${applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`,
}}
></div>
</div>
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
</div>
</CardHeader>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/uptime">View uptime metrics</Link>
</Button>
</CardFooter>
</Card>
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Network</CardTitle>
<Network className="h-6 w-6 text-sky-500" />
</div>
<CardDescription>Manage network configuration</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{serverCount + applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/network">View network details</Link>
</Button>
</CardFooter>
</Card>
</div>
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
<span className="text-gray-400 text-2xl">COMING SOON</span>
</div>
<div className="pt-4">
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
<span className="text-gray-400 text-2xl">COMING SOON</span>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}
)
}

View File

@ -67,6 +67,12 @@ import {
import Cookies from "js-cookie";
import { useState, useEffect } from "react";
import axios from "axios";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
interface Application {
id: number;
@ -254,7 +260,7 @@ export default function Dashboard() {
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
@ -275,9 +281,9 @@ export default function Dashboard() {
</Breadcrumb>
</div>
</header>
<div className="pl-4 pr-4">
<div className="p-6">
<div className="flex justify-between items-center">
<span className="text-2xl font-semibold">Your Applications</span>
<span className="text-3xl font-bold">Your Applications</span>
<div className="flex gap-2">
<Button
variant="outline"
@ -358,9 +364,18 @@ export default function Dashboard() {
placeholder="https://example.com/icon.png"
onChange={(e) => setIcon(e.target.value)}
/>
<Button variant="outline" size="icon" onClick={generateIconURL}>
<Zap />
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button variant="outline" size="icon" onClick={generateIconURL}>
<Zap />
</Button>
</TooltipTrigger>
<TooltipContent>
Generate Icon URL
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="grid w-full items-center gap-1.5">
@ -457,7 +472,9 @@ export default function Dashboard() {
</CardTitle>
<CardDescription className="text-md">
{app.description}
<br />
{app.description && (
<br className="hidden md:block" />
)}
Server: {app.server || "No server"}
</CardDescription>
</div>
@ -664,14 +681,14 @@ export default function Dashboard() {
</div>
</div>
)}
<div className="pt-4">
<div className="pt-4 pb-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={handlePrevious}
isActive={currentPage > 1}
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
/>
</PaginationItem>
<PaginationItem>
@ -679,9 +696,9 @@ export default function Dashboard() {
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
onClick={handleNext}
isActive={currentPage < maxPage}
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
/>
</PaginationItem>
</PaginationContent>

View File

@ -41,7 +41,7 @@ export default function Dashboard() {
<SidebarProvider>
<AppSidebar />
<SidebarInset className="flex flex-col h-screen">
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 dark:text-white" />
<Separator

View File

@ -107,6 +107,7 @@ export default function Dashboard() {
const [currentPage, setCurrentPage] = useState<number>(1);
const [maxPage, setMaxPage] = useState<number>(1);
const [itemsPerPage, setItemsPerPage] = useState<number>(4);
const [servers, setServers] = useState<Server[]>([]);
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
@ -126,7 +127,9 @@ export default function Dashboard() {
useEffect(() => {
const savedLayout = Cookies.get("layoutPreference-servers");
setIsGridLayout(savedLayout === "grid");
const layout_bool = savedLayout === "grid";
setIsGridLayout(layout_bool);
setItemsPerPage(layout_bool ? 6 : 4);
}, []);
const toggleLayout = () => {
@ -137,6 +140,7 @@ export default function Dashboard() {
path: "/",
sameSite: "strict",
});
setItemsPerPage(newLayout ? 6 : 4);
};
const add = async () => {
@ -164,6 +168,7 @@ export default function Dashboard() {
"/api/servers/get",
{
page: currentPage,
ITEMS_PER_PAGE: itemsPerPage,
}
);
setServers(response.data.servers);
@ -176,7 +181,7 @@ export default function Dashboard() {
useEffect(() => {
getServers();
}, [currentPage]);
}, [currentPage, itemsPerPage]);
const handlePrevious = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
@ -260,8 +265,8 @@ export default function Dashboard() {
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
@ -281,9 +286,9 @@ export default function Dashboard() {
</Breadcrumb>
</div>
</header>
<div className="pl-4 pr-4">
<div className="p-6">
<div className="flex justify-between items-center">
<span className="text-2xl font-semibold">Your Servers</span>
<span className="text-3xl font-bold">Your Servers</span>
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
@ -514,7 +519,7 @@ export default function Dashboard() {
<div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" />
<span>
<b>IP:</b> {server.ip || "Nicht angegeben"}
<b>IP:</b> {server.ip || "Not set"}
</span>
</div>
@ -750,14 +755,14 @@ export default function Dashboard() {
</div>
</div>
)}
<div className="pt-4">
<div className="pt-4 pb-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={handlePrevious}
isActive={currentPage > 1}
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
/>
</PaginationItem>
@ -767,9 +772,9 @@ export default function Dashboard() {
<PaginationItem>
<PaginationNext
href="#"
onClick={handleNext}
isActive={currentPage < maxPage}
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
/>
</PaginationItem>
</PaginationContent>

View File

@ -12,7 +12,7 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Card, CardHeader } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { useTheme } from "next-themes";
import {
Select,
@ -21,15 +21,121 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Input } from "@/components/ui/input"
import { useState } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle, Check, Palette, User } from "lucide-react";
export default function Settings() {
const { theme, setTheme } = useTheme();
const [email, setEmail] = useState<string>("")
const [password, setPassword] = useState<string>("")
const [confirmPassword, setConfirmPassword] = useState<string>("")
const [oldPassword, setOldPassword] = useState<string>("")
const [emailError, setEmailError] = useState<string>("")
const [passwordError, setPasswordError] = useState<string>("")
const [emailErrorVisible, setEmailErrorVisible] = useState<boolean>(false)
const [passwordErrorVisible, setPasswordErrorVisible] = useState<boolean>(false)
const [passwordSuccess, setPasswordSuccess] = useState<boolean>(false)
const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
const changeEmail = async () => {
setEmailErrorVisible(false);
setEmailSuccess(false);
setEmailError("");
if (!email) {
setEmailError("Email is required");
setEmailErrorVisible(true);
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}
, 3000);
return;
}
try {
await axios.post('/api/auth/edit_email', {
newEmail: email,
jwtToken: Cookies.get('token')
});
setEmailSuccess(true);
setEmail("");
setTimeout(() => {
setEmailSuccess(false);
}, 3000);
} catch (error: any) {
setEmailError(error.response.data.error);
setEmailErrorVisible(true);
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}, 3000);
}
}
const changePassword = async () => {
try {
if (password !== confirmPassword) {
setPasswordError("Passwords do not match");
setPasswordErrorVisible(true);
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
}
if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required");
setPasswordErrorVisible(true);
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
}
const response = await axios.post('/api/auth/edit_password', {
oldPassword: oldPassword,
newPassword: password,
jwtToken: Cookies.get('token')
});
if (response.status === 200) {
setPasswordSuccess(true);
setPassword("");
setOldPassword("");
setConfirmPassword("");
setTimeout(() => {
setPasswordSuccess(false);
}, 3000);
}
} catch (error: any) {
setPasswordErrorVisible(true);
setPasswordError(error.response.data.error);
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
}
}
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
@ -50,32 +156,142 @@ export default function Settings() {
</Breadcrumb>
</div>
</header>
<div className="pl-4 pr-4">
<span className="text-2xl font-semibold">Settings</span>
<div className="pt-4">
<Card className="w-full mb-4 relative">
<CardHeader>
<span className="text-xl font-bold">Theme</span>
<Select
value={theme}
onValueChange={(value: string) => setTheme(value)}
>
<SelectTrigger className="w-full [&_svg]:hidden">
<SelectValue>
{(theme ?? 'system').charAt(0).toUpperCase() + (theme ?? 'system').slice(1)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<div className="p-6">
<div className="pb-4">
<span className="text-3xl font-bold">Settings</span>
</div>
<div className="grid gap-6">
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" />
<h2 className="text-xl font-semibold">User Settings</h2>
</div>
</CardHeader>
<CardContent className="pb-6">
<div className="text-sm text-muted-foreground mb-6">
Manage your user settings here. You can change your email, password, and other account settings.
</div>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="font-semibold text-lg">Change Email</h3>
</div>
{emailErrorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{emailError}</AlertDescription>
</Alert>
)}
{emailSuccess && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
<Check className="h-4 w-4" />
<AlertTitle>Success</AlertTitle>
<AlertDescription>Email changed successfully.</AlertDescription>
</Alert>
)}
<div className="space-y-3">
<Input
type="email"
placeholder="Enter new email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="h-11"
/>
<Button onClick={changeEmail} className="w-full h-11">
Change Email
</Button>
</div>
</div>
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="font-semibold text-lg">Change Password</h3>
</div>
{passwordErrorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{passwordError}</AlertDescription>
</Alert>
)}
{passwordSuccess && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
<Check className="h-4 w-4" />
<AlertTitle>Success</AlertTitle>
<AlertDescription>Password changed successfully.</AlertDescription>
</Alert>
)}
<div className="space-y-3">
<Input
type="password"
placeholder="Enter old password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
className="h-11"
/>
<Input
type="password"
placeholder="Enter new password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="h-11"
/>
<Input
type="password"
placeholder="Confirm new password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="h-11"
/>
<Button onClick={changePassword} className="w-full h-11">
Change Password
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2">
<Palette className="h-5 w-5 text-primary" />
<h2 className="text-xl font-semibold">Theme Settings</h2>
</div>
</CardHeader>
<CardContent className="pb-6">
<div className="text-sm text-muted-foreground mb-6">
Select a theme for the application. You can choose between light, dark, or system theme.
</div>
<div className="max-w-md">
<Select value={theme} onValueChange={(value: string) => setTheme(value)}>
<SelectTrigger className="w-full h-11">
<SelectValue>
{(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}
)
}

View File

@ -0,0 +1,296 @@
import { AppSidebar } from "@/components/app-sidebar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { useEffect, useState } from "react";
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";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationLink,
} from "@/components/ui/pagination";
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 minBoxWidths = {
1: 24,
2: 24,
3: 24
};
interface UptimeData {
appName: string;
appId: number;
uptimeSummary: {
timestamp: string;
missing: boolean;
online: boolean | null;
}[];
}
interface PaginationData {
currentPage: number;
totalPages: number;
totalItems: number;
}
export default function Uptime() {
const [data, setData] = useState<UptimeData[]>([]);
const [timespan, setTimespan] = useState<1 | 2 | 3>(1);
const [pagination, setPagination] = useState<PaginationData>({
currentPage: 1,
totalPages: 1,
totalItems: 0
});
const [isLoading, setIsLoading] = useState(false);
const getData = async (selectedTimespan: number, page: number) => {
setIsLoading(true);
try {
const response = await axios.post<{
data: UptimeData[];
pagination: PaginationData;
}>("/api/applications/uptime", {
timespan: selectedTimespan,
page
});
setData(response.data.data);
setPagination(response.data.pagination);
} catch (error) {
console.error("Error:", error);
setData([]);
setPagination({
currentPage: 1,
totalPages: 1,
totalItems: 0
});
} finally {
setIsLoading(false);
}
};
const handlePrevious = () => {
const newPage = Math.max(1, pagination.currentPage - 1);
setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage);
};
const handleNext = () => {
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage);
};
useEffect(() => {
getData(timespan, 1);
}, [timespan]);
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>/</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Uptime</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="p-6">
<div className="flex justify-between items-center">
<span className="text-3xl font-bold">Uptime</span>
<Select
value={String(timespan)}
onValueChange={(v) => {
setTimespan(Number(v) as 1 | 2 | 3);
setPagination(prev => ({...prev, currentPage: 1}));
}}
disabled={isLoading}
>
<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">
{isLoading ? (
<div className="text-center py-8">Loading...</div>
) : (
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>
<Tooltip.Provider>
<div
className="grid gap-0.5 w-full pb-2"
style={{
gridTemplateColumns: `repeat(auto-fit, minmax(${minBoxWidths[timespan]}px, 1fr))`
}}
>
{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>
{pagination.totalItems > 0 && !isLoading && (
<div className="pt-4 pb-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={handlePrevious}
aria-disabled={pagination.currentPage === 1 || isLoading}
className={
pagination.currentPage === 1 || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink isActive>{pagination.currentPage}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={handleNext}
aria-disabled={pagination.currentPage === pagination.totalPages || isLoading}
className={
pagination.currentPage === pagination.totalPages || isLoading
? "opacity-50 cursor-not-allowed"
: "hover:cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
</SidebarInset>
</SidebarProvider>
);
}

View File

@ -0,0 +1,59 @@
"use client";
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import Uptime from "./Uptime";
import axios from "axios";
export default function DashboardPage() {
const router = useRouter();
const [isAuthChecked, setIsAuthChecked] = useState(false);
const [isValid, setIsValid] = useState(false);
useEffect(() => {
const token = Cookies.get("token");
if (!token) {
router.push("/");
} else {
const checkToken = async () => {
try {
const response = await axios.post("/api/auth/validate", {
token: token,
});
if (response.status === 200) {
setIsValid(true);
}
} catch (error: any) {
Cookies.remove("token");
router.push("/");
}
}
checkToken();
}
setIsAuthChecked(true);
}, [router]);
if (!isAuthChecked) {
return (
<div className="flex items-center justify-center h-screen">
<div className='inline-block' role='status' aria-label='loading'>
<svg className='w-6 h-6 stroke-white animate-spin ' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<g clip-path='url(#clip0_9023_61563)'>
<path d='M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444' stroke='stroke-current' stroke-width='1.4' stroke-linecap='round' className='my-path'></path>
</g>
<defs>
<clipPath id='clip0_9023_61563'>
<rect width='24' height='24' fill='white'></rect>
</clipPath>
</defs>
</svg>
<span className='sr-only'>Loading...</span>
</div>
</div>
)
}
return isValid ? <Uptime /> : null;
}

View File

@ -1,108 +1,148 @@
"use client";
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import Cookies from "js-cookie"
import { useRouter } from "next/navigation"
import axios from "axios"
import { AlertCircle, KeyRound, Mail, User } from "lucide-react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { AlertCircle } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert"
import { useState, useEffect } from "react";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import axios from "axios";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
export default function Home() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const [error, setError] = useState('');
const [errorVisible, setErrorVisible] = useState(false);
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [rememberMe, setRememberMe] = useState(false)
const router = useRouter()
const [error, setError] = useState("")
const [errorVisible, setErrorVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const token = Cookies.get('token');
const token = Cookies.get("token")
if (token) {
router.push('/dashboard');
router.push("/dashboard")
}
}, [router]);
}, [router])
interface LoginResponse {
token: string;
token: string
}
const login = async () => {
try {
const response = await axios.post('/api/auth/login', { username, password });
const { token } = response.data as LoginResponse;
Cookies.set('token', token);
router.push('/dashboard');
} catch (error: any) {
setError(error.response.data.error);
if (!username || !password) {
setError("Please enter both email and password")
setErrorVisible(true)
return
}
try {
setIsLoading(true)
const response = await axios.post("/api/auth/login", { username, password })
const { token } = response.data as LoginResponse
const cookieOptions = rememberMe ? { expires: 7 } : {}
Cookies.set("token", token, cookieOptions)
router.push("/dashboard")
} catch (error: any) {
setError(error.response?.data?.error || "Login failed. Please try again.")
setErrorVisible(true)
} finally {
setIsLoading(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
login()
}
}
return (
<div className="flex flex-col min-h-screen items-center justify-center gap-6 ">
<Card className="w-1/3">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your Login data of the compose.yml file below to access
</CardDescription>
</CardHeader>
<CardContent>
{errorVisible && (
<>
<div className="pb-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error}
</AlertDescription>
</Alert>
</div>
</>
)}
<div>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="mail@example.com"
required
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input id="password" type="password" required placeholder="* * * * * * *" onChange={(e) => setPassword(e.target.value)}/>
</div>
<Button className="w-full" onClick={login}>
Login
</Button>
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/30 p-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center space-y-2">
<div className="flex justify-center">
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center">
<KeyRound className="h-8 w-8 text-primary" />
</div>
</div>
</CardContent>
</Card>
<h1 className="text-4xl font-bold tracking-tight text-foreground">CoreControl</h1>
<p className="text-muted-foreground">Sign in to access your dashboard</p>
</div>
<Card className="border-muted/40 shadow-lg">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-semibold">Login</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{errorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50 slide-in-from-top-5">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Authentication Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
<Input
id="email"
type="email"
placeholder="mail@example.com"
className="pl-10"
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={handleKeyDown}
required
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password" className="text-sm font-medium">
Password
</Label>
</div>
<div className="relative">
<User className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
<Input
id="password"
type="password"
placeholder="••••••••"
className="pl-10"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
required
/>
</div>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-col space-y-4">
<Button className="w-full" onClick={login} disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign in"}
</Button>
</CardFooter>
</Card>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import Image from "next/image"
import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network } from "lucide-react"
import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network, Activity } from "lucide-react"
import {
Sidebar,
SidebarContent,
@ -50,6 +50,11 @@ const data: { navMain: NavItem[] } = {
icon: AppWindow,
url: "/dashboard/applications",
},
{
title: "Uptime",
icon: Activity,
url: "/dashboard/uptime",
},
{
title: "Network",
icon: Network,

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -4,10 +4,7 @@ services:
ports:
- "3000:3000"
environment:
LOGIN_EMAIL: "mail@example.com"
LOGIN_PASSWORD: "SecretPassword"
JWT_SECRET: RANDOM_SECRET
ACCOUNT_SECRET: RANDOM_SECRET
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
- db

718
package-lock.json generated
View File

@ -1,15 +1,16 @@
{
"name": "corecontrol",
"version": "0.1.0",
"version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "corecontrol",
"version": "0.1.0",
"version": "0.0.4",
"dependencies": {
"@prisma/client": "^6.6.0",
"@prisma/extension-accelerate": "^1.3.0",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
@ -20,10 +21,12 @@
"@radix-ui/react-tabs": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.0",
"@types/axios": "^0.9.36",
"@types/bcrypt": "^5.0.2",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.9",
"@xyflow/react": "^12.5.5",
"axios": "^1.8.4",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",
@ -62,9 +65,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz",
"integrity": "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz",
"integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==",
"license": "MIT",
"optional": true,
"dependencies": {
@ -911,6 +914,26 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@next/env": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.0.tgz",
@ -1029,22 +1052,6 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@prisma/client": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz",
@ -1151,6 +1158,37 @@
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.4.tgz",
"integrity": "sha512-SGCxlSBaMvEzDROzyZjsVNzu9XY5E28B3k8jOENyrz6csOv/pG1eHyYfLJai1n9tRjwG61coXDhfpgtxKxUv5g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collapsible": "1.1.4",
"@radix-ui/react-collection": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-controllable-state": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.7.tgz",
@ -1202,6 +1240,36 @@
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz",
"integrity": "sha512-u7LCw1EYInQtBNLGjm9nZ89S/4GcvX1UR5XbekEgnQae2Hkpq39ycJ1OhdeN1/JDfVNG91kWaWoest127TaEKQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-controllable-state": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz",
@ -2135,7 +2203,7 @@
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"node_modules/@tailwindcss/oxide/node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.3.tgz",
"integrity": "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==",
@ -2172,6 +2240,15 @@
"integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
"license": "MIT"
},
"node_modules/@types/bcrypt": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
@ -2253,9 +2330,9 @@
}
},
"node_modules/@types/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
"integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==",
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
"integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@ -2273,12 +2350,12 @@
}
},
"node_modules/@xyflow/react": {
"version": "12.5.5",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.5.tgz",
"integrity": "sha512-mAtHuS4ktYBL1ph5AJt7X/VmpzzlmQBN3+OXxyT/1PzxwrVto6AKc3caerfxzwBsg3cA4J8lB63F3WLAuPMmHw==",
"version": "12.5.6",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.6.tgz",
"integrity": "sha512-a6lL0WoeMSp7AC9AQzWMRMuqk12Dn+lVjMDLL93SZvpWv5D2BSq9woCv21JCUdWQ31MNpJVfLaV3TycaH1tsYw==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.55",
"@xyflow/system": "0.0.56",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@ -2288,9 +2365,9 @@
}
},
"node_modules/@xyflow/system": {
"version": "0.0.55",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.55.tgz",
"integrity": "sha512-6cngWlE4oMXm+zrsbJxerP3wUNUFJcv/cE5kDfu0qO55OWK3fAeSOLW9td3xEVQlomjIW5knds1MzeMnBeCfqw==",
"version": "0.0.56",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.56.tgz",
"integrity": "sha512-Xc3LvEumjJD+CqPqlYkrlszJ4hWQ0DE+r5M4e5WpS/hKT4T6ktAjt7zeMNJ+vvTsXHulGnEoDRA8zbIfB6tPdQ==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
@ -2302,6 +2379,53 @@
"d3-zoom": "^3.0.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-hidden": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
@ -2331,6 +2455,36 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@ -2381,6 +2535,15 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@ -2459,6 +2622,15 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -2471,6 +2643,18 @@
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -2587,7 +2771,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -2610,11 +2793,16 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@ -2649,6 +2837,12 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@ -2797,6 +2991,36 @@
"node": ">= 6"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2830,6 +3054,27 @@
"node": ">=10"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -2889,6 +3134,27 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -2935,6 +3201,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -2947,6 +3219,36 @@
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@ -2954,6 +3256,15 @@
"license": "MIT",
"optional": true
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@ -3234,7 +3545,7 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"node_modules/lightningcss/node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
@ -3306,6 +3617,30 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -3336,6 +3671,64 @@
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3424,6 +3817,22 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -3452,6 +3861,87 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -3612,6 +4102,20 @@
}
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -3622,6 +4126,22 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -3660,6 +4180,12 @@
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz",
@ -3701,6 +4227,12 @@
"@img/sharp-win32-x64": "0.34.1"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@ -3728,6 +4260,41 @@
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@ -3778,6 +4345,29 @@
"node": ">=6"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -3885,6 +4475,49 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
@ -3912,6 +4545,21 @@
"optional": true
}
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "corecontrol",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@ -11,6 +11,7 @@
"dependencies": {
"@prisma/client": "^6.6.0",
"@prisma/extension-accelerate": "^1.3.0",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
@ -21,10 +22,12 @@
"@radix-ui/react-tabs": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.0",
"@types/axios": "^0.9.36",
"@types/bcrypt": "^5.0.2",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.9",
"@xyflow/react": "^12.5.5",
"axios": "^1.8.4",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",

View File

@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "user" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");

View File

@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "uptime_history" (
"id" SERIAL NOT NULL,
"applicationId" INTEGER NOT NULL DEFAULT 1,
"online" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "uptime_history_pkey" PRIMARY KEY ("id")
);

View File

@ -25,6 +25,13 @@ model application {
online Boolean @default(true)
}
model uptime_history {
id Int @id @default(autoincrement())
applicationId Int @default(1)
online Boolean @default(true)
createdAt DateTime @default(now())
}
model server {
id Int @id @default(autoincrement())
name String
@ -38,6 +45,12 @@ model server {
}
model settings {
id Int @id @default(autoincrement())
uptime_checks Boolean @default(true)
id Int @id @default(autoincrement())
uptime_checks Boolean @default(true)
}
model user {
id String @id @default(uuid())
email String @unique
password String
}

BIN
public/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB