From 07e7a60163256ec035cb589cf031e49a6762de4b Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 18:51:47 +0200 Subject: [PATCH 01/55] Agent .dockerignore Update --- agent/.dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/.dockerignore b/agent/.dockerignore index 3a8fe5e..2eea525 100644 --- a/agent/.dockerignore +++ b/agent/.dockerignore @@ -1 +1 @@ -.env.local \ No newline at end of file +.env \ No newline at end of file From fdb8b5073fba9911248af78375070882d5009f89 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 18:53:34 +0200 Subject: [PATCH 02/55] Version Update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac39468..bcaabbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corecontrol", - "version": "0.0.3", + "version": "0.0.4", "private": true, "scripts": { "dev": "next dev --turbopack", From 7549d8c8c0d6e2860288664731c4b1bcf3c1f63f Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 18:54:31 +0200 Subject: [PATCH 03/55] Readme.md: selfh.st shout-out --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3ba1298..9e5fb86 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,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 From 246f6b594cafc9c43758430b5a2a72dde8457174 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 19:44:16 +0200 Subject: [PATCH 04/55] DB managed user --- app/api/auth/login/route.ts | 47 +- app/api/auth/validate/route.ts | 12 +- package-lock.json | 656 +++++++++++++++++- package.json | 2 + .../20250414173201_user_model/migration.sql | 11 + prisma/schema.prisma | 10 +- 6 files changed, 693 insertions(+), 45 deletions(-) create mode 100644 prisma/migrations/20250414173201_user_model/migration.sql diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index af7a928..0e55791 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -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) { diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts index 14ca8b3..0b25efa 100644 --- a/app/api/auth/validate/route.ts +++ b/app/api/auth/validate/route.ts @@ -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 }); } diff --git a/package-lock.json b/package-lock.json index 50d007a..291a57e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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", @@ -20,10 +20,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 +64,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 +913,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 +1051,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", @@ -2135,7 +2141,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 +2178,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 +2268,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 +2288,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 +2303,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 +2317,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 +2393,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 +2473,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 +2560,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 +2581,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 +2709,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 +2731,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 +2775,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 +2929,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 +2992,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 +3072,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 +3139,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 +3157,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 +3194,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 +3483,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 +3555,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 +3609,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 +3755,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 +3799,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 +4040,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 +4064,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 +4118,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 +4165,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 +4198,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 +4283,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 +4413,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 +4483,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" + } } } } diff --git a/package.json b/package.json index bcaabbe..3e71449 100644 --- a/package.json +++ b/package.json @@ -21,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", diff --git a/prisma/migrations/20250414173201_user_model/migration.sql b/prisma/migrations/20250414173201_user_model/migration.sql new file mode 100644 index 0000000..b81375d --- /dev/null +++ b/prisma/migrations/20250414173201_user_model/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cad3932..e96f1bd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,6 +38,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 } \ No newline at end of file From a1d9839bccce5bf68d49391325db07ce9abd40fd Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 20:02:28 +0200 Subject: [PATCH 05/55] Settings accordion --- app/dashboard/settings/Settings.tsx | 45 +++++++++++------ components/ui/accordion.tsx | 66 +++++++++++++++++++++++++ package-lock.json | 77 +++++++++++++++++++++++------ package.json | 1 + 4 files changed, 158 insertions(+), 31 deletions(-) create mode 100644 components/ui/accordion.tsx diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index b4d0ece..665c7c3 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -21,6 +21,12 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" export default function Settings() { const { theme, setTheme } = useTheme(); @@ -55,22 +61,29 @@ export default function Settings() {
- Theme - + + + Theme + +
Select a theme for the application. You can choose between light, dark, or system theme.
+ +
+
+
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..4a8cca4 --- /dev/null +++ b/components/ui/accordion.tsx @@ -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) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/package-lock.json b/package-lock.json index 291a57e..f295e81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,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", @@ -1157,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", @@ -1208,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", @@ -4483,21 +4545,6 @@ "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" - } } } } diff --git a/package.json b/package.json index 3e71449..72dbb10 100644 --- a/package.json +++ b/package.json @@ -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", From 49fec67996e1a226feb8385919e4b438b0c8cb96 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 20:15:44 +0200 Subject: [PATCH 06/55] Edit Email API Route --- app/api/auth/edit_email/route.ts | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/api/auth/edit_email/route.ts diff --git a/app/api/auth/edit_email/route.ts b/app/api/auth/edit_email/route.ts new file mode 100644 index 0000000..92e4bd7 --- /dev/null +++ b/app/api/auth/edit_email/route.ts @@ -0,0 +1,57 @@ +import { NextResponse, NextRequest } from "next/server"; +import jwt from 'jsonwebtoken'; +import { prisma } from "@/lib/prisma"; +import bcrypt from 'bcrypt'; + +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 }); + } +} \ No newline at end of file From 5413dbf9480cbb8e5b241c4e4c110e704120eb8c Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 20:20:42 +0200 Subject: [PATCH 07/55] Edit Password API Route --- app/api/auth/edit_email/route.ts | 1 - app/api/auth/edit_password/route.ts | 55 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 app/api/auth/edit_password/route.ts diff --git a/app/api/auth/edit_email/route.ts b/app/api/auth/edit_email/route.ts index 92e4bd7..73fccd1 100644 --- a/app/api/auth/edit_email/route.ts +++ b/app/api/auth/edit_email/route.ts @@ -1,7 +1,6 @@ import { NextResponse, NextRequest } from "next/server"; import jwt from 'jsonwebtoken'; import { prisma } from "@/lib/prisma"; -import bcrypt from 'bcrypt'; interface EditEmailRequest { newEmail: string; diff --git a/app/api/auth/edit_password/route.ts b/app/api/auth/edit_password/route.ts new file mode 100644 index 0000000..dd162a8 --- /dev/null +++ b/app/api/auth/edit_password/route.ts @@ -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 }); + } +} \ No newline at end of file From 6661b1e7118946d382b2052181edb777f9ab2e45 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 20:59:41 +0200 Subject: [PATCH 08/55] Change Email & password Function --- app/dashboard/settings/Settings.tsx | 198 ++++++++++++++++++++++++++++ package-lock.json | 15 +++ 2 files changed, 213 insertions(+) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 665c7c3..7c7a7c7 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -27,10 +27,110 @@ import { 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 } from "lucide-react"; export default function Settings() { const { theme, setTheme } = useTheme(); + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [oldPassword, setOldPassword] = useState("") + + const [emailError, setEmailError] = useState("") + const [passwordError, setPasswordError] = useState("") + const [emailErrorVisible, setEmailErrorVisible] = useState(false) + const [passwordErrorVisible, setPasswordErrorVisible] = useState(false) + + const [passwordSuccess, setPasswordSuccess] = useState(false) + const [emailSuccess, setEmailSuccess] = useState(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 ( @@ -63,6 +163,104 @@ export default function Settings() { + User + +
Manage your user settings here. You can change your email, password, and other account settings.
+
+
+
Change Email
+ { emailErrorVisible && +
+ + + Error + + {emailError} + + +
+ } + { emailSuccess && +
+ + + Success + + Email changed successfully. + + +
+ } + setEmail(e.target.value)} + className="mb-2" + /> + +
+
+
Change Password
+ { passwordErrorVisible && +
+ + + Error + + {passwordError} + + +
+ } + { passwordSuccess && +
+ + + Success + + Password changed successfully. + + +
+ } + setOldPassword(e.target.value)} + className="mb-2" + /> + setPassword(e.target.value)} + className="mb-2" + /> + setConfirmPassword(e.target.value)} + className="mb-2" + /> + +
+
+
+
+ Theme
Select a theme for the application. You can choose between light, dark, or system theme.
diff --git a/package-lock.json b/package-lock.json index f295e81..b4859da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4545,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" + } } } } From ca31a0b6b35ee593238f14e760f50efe37e64619 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 20:59:59 +0200 Subject: [PATCH 09/55] Login Page Update --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 44bef4d..608d110 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -60,7 +60,7 @@ export default function Home() { Login - Enter your Login data of the compose.yml file below to access + Enter your email and password to login. From 7c86483d4899dd00f6109b6a165483cc157cd8d2 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:10:21 +0200 Subject: [PATCH 10/55] Generate icon url tooltip --- app/dashboard/applications/Applications.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 0ac2871..df697d9 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -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; @@ -358,9 +364,18 @@ export default function Dashboard() { placeholder="https://example.com/icon.png" onChange={(e) => setIcon(e.target.value)} /> - + + + + + + + Generate Icon URL + + +
From 0a8ea98dae79854228149116910c97b8f0582b24 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:12:12 +0200 Subject: [PATCH 11/55] Application Description Improvement --- app/dashboard/applications/Applications.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index df697d9..4f8197f 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -472,7 +472,9 @@ export default function Dashboard() { {app.description} -
+ {app.description && ( +
+ )} Server: {app.server || "No server"}
From 8fc4fea687fed1538964e81cde0f78476d213bcd Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:26:12 +0200 Subject: [PATCH 12/55] Servers ITEMS_PER_PAGE --- app/api/servers/get/route.ts | 5 +++-- app/dashboard/servers/Servers.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts index 64fa480..bbeaf22 100644 --- a/app/api/servers/get/route.ts +++ b/app/api/servers/get/route.ts @@ -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, diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index 9b5853f..ee55290 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -107,6 +107,7 @@ export default function Dashboard() { const [currentPage, setCurrentPage] = useState(1); const [maxPage, setMaxPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(4); const [servers, setServers] = useState([]); const [isGridLayout, setIsGridLayout] = useState(false); const [loading, setLoading] = useState(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)); From e51600016b8ca08cb9c97e36c71b6b3406158f5c Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:28:09 +0200 Subject: [PATCH 13/55] Pagination padding bottom --- app/dashboard/applications/Applications.tsx | 2 +- app/dashboard/servers/Servers.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 4f8197f..512a844 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -681,7 +681,7 @@ export default function Dashboard() { )} -
+
diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index ee55290..8255792 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -755,7 +755,7 @@ export default function Dashboard() {
)} -
+
From 36beeb8a2c69d3ff28382e559c0ece3fef6ab933 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:36:15 +0200 Subject: [PATCH 14/55] Pagination Button Improvements --- app/dashboard/applications/Applications.tsx | 4 ++-- app/dashboard/servers/Servers.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 512a844..3cd4199 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -686,9 +686,9 @@ export default function Dashboard() { 1} + style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }} /> @@ -696,9 +696,9 @@ export default function Dashboard() { diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index 8255792..00b4014 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -760,9 +760,9 @@ export default function Dashboard() { 1} + style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }} /> @@ -772,9 +772,9 @@ export default function Dashboard() { From e844712c29eb53914be6ecef7f17735a2887e8b6 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:48:09 +0200 Subject: [PATCH 15/55] Network Chart Styling Update --- app/api/flowchart/route.ts | 76 +++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/app/api/flowchart/route.ts b/app/api/flowchart/route.ts index 455bff7..737dbfb 100644 --- a/app/api/flowchart/route.ts +++ b/app/api/flowchart/route.ts @@ -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, ]); + // 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) { From d779355c4c79b38eb52f9cabe7bd03408bf4eee0 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 21:54:25 +0200 Subject: [PATCH 16/55] Prevent deletions of servers with associated applications --- app/api/servers/delete/route.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/api/servers/delete/route.ts b/app/api/servers/delete/route.ts index 82265da..ed7b852 100644 --- a/app/api/servers/delete/route.ts +++ b/app/api/servers/delete/route.ts @@ -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 } From 7da6501ca7b237713c4699efe4908bb2c3ee1ec7 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 22:16:22 +0200 Subject: [PATCH 17/55] Uptime History DB Model --- .../20250414201604_uptime_history/migration.sql | 9 +++++++++ prisma/schema.prisma | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 prisma/migrations/20250414201604_uptime_history/migration.sql diff --git a/prisma/migrations/20250414201604_uptime_history/migration.sql b/prisma/migrations/20250414201604_uptime_history/migration.sql new file mode 100644 index 0000000..3021ba4 --- /dev/null +++ b/prisma/migrations/20250414201604_uptime_history/migration.sql @@ -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") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e96f1bd..55fe57d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 From 75d1bd59f47f035f714611b407c06955bf887b15 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 22:31:41 +0200 Subject: [PATCH 18/55] Readme.md Roadmap Update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e5fb86..a720dfa 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ Network Page: ![Network Page](https://i.ibb.co/XkKYrGQX/image.png) ## Roadmap -- [ ] Edit Applications, Applications searchbar +- [X] Edit Applications, Applications searchbar - [ ] Customizable Dashboard - [ ] Notifications -- [ ] Uptime History +- [X] Uptime History - [ ] Simple Server Monitoring - [ ] Improved Network Flowchart with custom elements (like Network switches) - [ ] Advanced Settings (Disable Uptime Tracking & more) From 19ef051e1e891a710d4ebb4b9e72a0b972c68497 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Mon, 14 Apr 2025 23:36:33 +0200 Subject: [PATCH 19/55] Docker Compose Update --- README.md | 3 --- compose.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/README.md b/README.md index a720dfa..fb6a650 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,7 @@ services: ports: - "3000:3000" environment: - LOGIN_EMAIL: "mail@example.com" - LOGIN_PASSWORD: "SecretPassword" JWT_SECRET: RANDOM_SECRET - ACCOUNT_SECRET: RANDOM_SECRET DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db diff --git a/compose.yml b/compose.yml index a7a10df..7475da7 100644 --- a/compose.yml +++ b/compose.yml @@ -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 DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db From 3f1f7b730ef4178aa2bbcf121a37053dd4c405db Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 11:37:30 +0200 Subject: [PATCH 20/55] uptime_history for agent --- agent/main.go | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/agent/main.go b/agent/main.go index 5619f9d..3fca4da 100644 --- a/agent/main.go +++ b/agent/main.go @@ -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) + } } } From faad5198a641fbab902cbc0430ec4e99ddc26623 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 12:17:14 +0200 Subject: [PATCH 21/55] Activity Tab Sidebar --- components/app-sidebar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 19a4f3d..4dc59b9 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -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: "Activity", + icon: Activity, + url: "/dashboard/activity", + }, { title: "Network", icon: Network, From 52f3c0432fe51df5d2fe0a04e91aca4457aed609 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 12:20:03 +0200 Subject: [PATCH 22/55] Uptime SIdebar Fix --- components/app-sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 4dc59b9..a616da3 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -51,9 +51,9 @@ const data: { navMain: NavItem[] } = { url: "/dashboard/applications", }, { - title: "Activity", + title: "Uptime", icon: Activity, - url: "/dashboard/activity", + url: "/dashboard/uptime", }, { title: "Network", From 1a80c61c34fabd58766175aed1d2868ca3ce517e Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 12:22:15 +0200 Subject: [PATCH 23/55] Uptime Page --- app/dashboard/uptime/Uptime.tsx | 61 +++++++++++++++++++++++++++++++++ app/dashboard/uptime/page.tsx | 59 +++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 app/dashboard/uptime/Uptime.tsx create mode 100644 app/dashboard/uptime/page.tsx diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx new file mode 100644 index 0000000..fd469f6 --- /dev/null +++ b/app/dashboard/uptime/Uptime.tsx @@ -0,0 +1,61 @@ +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"; + +interface StatsResponse { + serverCount: number; + applicationCount: number; + onlineApplicationsCount: number; +} + +export default function Uptime() { + + return ( + + + +
+
+ + + + + + + / + + + + + My Infrastructure + + + + Uptime + + + +
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/uptime/page.tsx b/app/dashboard/uptime/page.tsx new file mode 100644 index 0000000..e397f3a --- /dev/null +++ b/app/dashboard/uptime/page.tsx @@ -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 ( +
+
+ + + + + + + + + + + Loading... +
+
+ ) + } + + return isValid ? : null; +} \ No newline at end of file From ed46598c27de5366eea8e7661e58a531c6606b60 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 12:32:49 +0200 Subject: [PATCH 24/55] Uptime History Page --- app/dashboard/uptime/Uptime.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index fd469f6..96d5739 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -53,7 +53,16 @@ export default function Uptime() {
- + Uptime +
+ + +
+ Application Name - Uptime +
+
+
+
From 2f6957a45db96ac4cad1ddb27ffd1cd28eb0a0f1 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:46:25 +0200 Subject: [PATCH 25/55] Uptime functionality --- app/api/applications/uptime/route.ts | 145 ++++++++++++++++++++++++ app/dashboard/uptime/Uptime.tsx | 161 ++++++++++++++++++++++++--- 2 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 app/api/applications/uptime/route.ts diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts new file mode 100644 index 0000000..00950b3 --- /dev/null +++ b/app/api/applications/uptime/route.ts @@ -0,0 +1,145 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +interface RequestBody { + timespan?: number; +} + +const getTimeRange = (timespan: number) => { + const now = new Date(); + switch (timespan) { + case 1: // 30 Minuten + return { + start: new Date(now.getTime() - 30 * 60 * 1000), + interval: 'minute' + }; + case 2: // 7 Tage + return { + start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), + interval: '3hour' + }; + case 3: // 30 Tage + return { + start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), + interval: 'day' + }; + default: + return { + start: new Date(now.getTime() - 30 * 60 * 1000), + interval: 'minute' + }; + } +}; + +const generateIntervals = (timespan: number) => { + const now = new Date(); + now.setSeconds(0, 0); + + switch (timespan) { + case 1: // 30 Minuten + return Array.from({ length: 30 }, (_, i) => { + const d = new Date(now); + d.setMinutes(d.getMinutes() - i); + d.setSeconds(0, 0); + return d; + }); + + case 2: // 7 Tage (56 Intervalle à 3 Stunden) + return Array.from({ length: 56 }, (_, i) => { + const d = new Date(now); + d.setHours(d.getHours() - (i * 3)); + d.setMinutes(0, 0, 0); + return d; + }); + + case 3: // 30 Tage + return Array.from({ length: 30 }, (_, i) => { + const d = new Date(now); + d.setDate(d.getDate() - i); + d.setHours(0, 0, 0, 0); + return d; + }); + + default: + return []; + } +}; + +const getIntervalKey = (date: Date, timespan: number) => { + const d = new Date(date); + switch (timespan) { + case 1: + d.setSeconds(0, 0); + return d.toISOString(); + case 2: + d.setHours(Math.floor(d.getHours() / 3) * 3); + d.setMinutes(0, 0, 0); + return d.toISOString(); + case 3: + d.setHours(0, 0, 0, 0); + return d.toISOString(); + default: + return d.toISOString(); + } +}; + +export async function POST(request: NextRequest) { + try { + const { timespan = 1 }: RequestBody = await request.json(); + const { start } = getTimeRange(timespan); + + const applications = await prisma.application.findMany(); + const uptimeHistory = await prisma.uptime_history.findMany({ + where: { + applicationId: { in: applications.map(app => app.id) }, + createdAt: { gte: start } + }, + orderBy: { createdAt: "desc" } + }); + + const intervals = generateIntervals(timespan); + + const result = applications.map(app => { + const appChecks = uptimeHistory.filter(check => check.applicationId === app.id); + const checksMap = new Map(); + + for (const check of appChecks) { + const intervalKey = getIntervalKey(check.createdAt, timespan); + const current = checksMap.get(intervalKey) || { failed: 0, total: 0 }; + current.total++; + if (!check.online) current.failed++; + checksMap.set(intervalKey, current); + } + + const uptimeSummary = intervals.map(interval => { + const intervalKey = getIntervalKey(interval, timespan); + const stats = checksMap.get(intervalKey); + + if (!stats) { + return { + timestamp: interval.toISOString(), + missing: true, + online: null + }; + } + + return { + timestamp: intervalKey, + missing: false, + online: stats.failed < 3 + }; + }); + + return { + appName: app.name, + appId: app.id, + uptimeSummary + }; + }); + + return NextResponse.json(result); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 96d5739..21e37d3 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -14,17 +14,57 @@ import { SidebarTrigger, } from "@/components/ui/sidebar"; import { useEffect, useState } from "react"; -import axios from "axios"; // Korrekter Import +import axios from "axios"; import { Card, CardHeader } from "@/components/ui/card"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -interface StatsResponse { - serverCount: number; - applicationCount: number; - onlineApplicationsCount: number; -} +const timeFormats = { + 1: (timestamp: string) => + new Date(timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false + }), + 2: (timestamp: string) => { + const start = new Date(timestamp); + const end = new Date(start.getTime() + 3 * 60 * 60 * 1000); + return `${start.toLocaleDateString([], { day: '2-digit', month: 'short' })} + ${start.getHours().toString().padStart(2, '0')}:00 - + ${end.getHours().toString().padStart(2, '0')}:00`; + }, + 3: (timestamp: string) => + new Date(timestamp).toLocaleDateString([], { + day: '2-digit', + month: 'short' + }) +}; + +const gridColumns = { + 1: 30, + 2: 56, + 3: 30 +}; export default function Uptime() { - + const [data, setData] = useState([]); + const [timespan, setTimespan] = useState<1 | 2 | 3>(1); + + const getData = async (selectedTimespan: number) => { + try { + const response = await axios.post("/api/applications/uptime", { + timespan: selectedTimespan + }); + setData(response.data); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + + useEffect(() => { + getData(timespan); + }, [timespan]); + return ( @@ -36,9 +76,7 @@ export default function Uptime() { - - / - + / @@ -53,16 +91,105 @@ export default function Uptime() {
+
Uptime -
- - -
- Application Name - Uptime + +
+
+ {data.map((app) => { + const reversedSummary = [...app.uptimeSummary].reverse(); + const startTime = reversedSummary[0]?.timestamp; + const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; + + return ( + + +
+
+ {app.appName} +
+ +
+
+ {startTime ? timeFormats[timespan](startTime) : ""} + {endTime ? timeFormats[timespan](endTime) : ""}
- + + +
+ {reversedSummary.map((entry) => ( + + +
+ + + +
+

+ {timespan === 2 ? ( + timeFormats[2](entry.timestamp) + ) : ( + new Date(entry.timestamp).toLocaleString([], { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: timespan === 3 ? undefined : '2-digit', + hour12: false + }) + )} +

+

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

+
+ +
+
+ + ))} +
+ +
+
+ -
+ ); + })} +
From 12abe9c0d7a1f2d0e2c07e9800a86260c6deab7d Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:52:46 +0200 Subject: [PATCH 26/55] Uptime.tsx Type Error Fix --- app/dashboard/uptime/Uptime.tsx | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 21e37d3..3bae2d7 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -46,20 +46,31 @@ const gridColumns = { 3: 30 }; +interface UptimeData { + appName: string; + appId: number; + uptimeSummary: { + timestamp: string; + missing: boolean; + online: boolean | null; + }[]; + } + export default function Uptime() { - const [data, setData] = useState([]); - const [timespan, setTimespan] = useState<1 | 2 | 3>(1); + const [data, setData] = useState([]); + const [timespan, setTimespan] = useState<1 | 2 | 3>(1); - const getData = async (selectedTimespan: number) => { - try { - const response = await axios.post("/api/applications/uptime", { - timespan: selectedTimespan - }); - setData(response.data); - } catch (error) { - console.error("Error fetching data:", error); - } - }; + const getData = async (selectedTimespan: number) => { + try { + const response = await axios.post("/api/applications/uptime", { + timespan: selectedTimespan + }); + setData(response.data); + } catch (error) { + console.error("Error:", error); + setData([]); // Setze leeres Array bei Fehlern + } + }; useEffect(() => { getData(timespan); From 4aaefe4c55474308ea9d17fd5a3e76347ad653e1 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:53:32 +0200 Subject: [PATCH 27/55] Remove unnecessary Code comments --- app/api/applications/uptime/route.ts | 12 ++++++------ app/dashboard/uptime/Uptime.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts index 00950b3..4f7510e 100644 --- a/app/api/applications/uptime/route.ts +++ b/app/api/applications/uptime/route.ts @@ -8,17 +8,17 @@ interface RequestBody { const getTimeRange = (timespan: number) => { const now = new Date(); switch (timespan) { - case 1: // 30 Minuten + case 1: return { start: new Date(now.getTime() - 30 * 60 * 1000), interval: 'minute' }; - case 2: // 7 Tage + case 2: return { start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), interval: '3hour' }; - case 3: // 30 Tage + case 3: return { start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), interval: 'day' @@ -36,7 +36,7 @@ const generateIntervals = (timespan: number) => { now.setSeconds(0, 0); switch (timespan) { - case 1: // 30 Minuten + case 1: return Array.from({ length: 30 }, (_, i) => { const d = new Date(now); d.setMinutes(d.getMinutes() - i); @@ -44,7 +44,7 @@ const generateIntervals = (timespan: number) => { return d; }); - case 2: // 7 Tage (56 Intervalle à 3 Stunden) + case 2: return Array.from({ length: 56 }, (_, i) => { const d = new Date(now); d.setHours(d.getHours() - (i * 3)); @@ -52,7 +52,7 @@ const generateIntervals = (timespan: number) => { return d; }); - case 3: // 30 Tage + case 3: return Array.from({ length: 30 }, (_, i) => { const d = new Date(now); d.setDate(d.getDate() - i); diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 3bae2d7..6c7f404 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -68,7 +68,7 @@ export default function Uptime() { setData(response.data); } catch (error) { console.error("Error:", error); - setData([]); // Setze leeres Array bei Fehlern + setData([]); } }; From f340543625c46780f8d52e9b933e54d0255c3d43 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:58:53 +0200 Subject: [PATCH 28/55] Uptime Pagination --- app/dashboard/uptime/Uptime.tsx | 102 ++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 6c7f404..90d5ab4 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -18,6 +18,13 @@ 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, +} from "@/components/ui/pagination"; const timeFormats = { 1: (timestamp: string) => @@ -47,30 +54,47 @@ const gridColumns = { }; interface UptimeData { - appName: string; - appId: number; - uptimeSummary: { - timestamp: string; - missing: boolean; - online: boolean | null; - }[]; - } - -export default function Uptime() { - const [data, setData] = useState([]); - const [timespan, setTimespan] = useState<1 | 2 | 3>(1); + appName: string; + appId: number; + uptimeSummary: { + timestamp: string; + missing: boolean; + online: boolean | null; + }[]; +} - const getData = async (selectedTimespan: number) => { - try { - const response = await axios.post("/api/applications/uptime", { - timespan: selectedTimespan - }); - setData(response.data); - } catch (error) { - console.error("Error:", error); - setData([]); - } - }; +export default function Uptime() { + const [data, setData] = useState([]); + const [timespan, setTimespan] = useState<1 | 2 | 3>(1); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 5; + + const maxPage = Math.ceil(data.length / itemsPerPage); + const paginatedData = data.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + const getData = async (selectedTimespan: number) => { + try { + const response = await axios.post("/api/applications/uptime", { + timespan: selectedTimespan + }); + setData(response.data); + setCurrentPage(1); // Reset page when timespan changes + } catch (error) { + console.error("Error:", error); + setData([]); + } + }; + + const handlePrevious = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; + + const handleNext = () => { + setCurrentPage((prev) => Math.min(maxPage, prev + 1)); + }; useEffect(() => { getData(timespan); @@ -118,8 +142,8 @@ export default function Uptime() {
-
- {data.map((app) => { +
+ {paginatedData.map((app) => { const reversedSummary = [...app.uptimeSummary].reverse(); const startTime = reversedSummary[0]?.timestamp; const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; @@ -201,6 +225,34 @@ export default function Uptime() { ); })}
+ + {data.length > 0 && ( +
+ + + + + + + + Page {currentPage} of {maxPage} + + + + + + + +
+ )}
From e34407539a9c231b55c700b18dade6090c3800b4 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 13:59:02 +0200 Subject: [PATCH 29/55] Remove comment --- app/dashboard/uptime/Uptime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 90d5ab4..b00a325 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -81,7 +81,7 @@ export default function Uptime() { timespan: selectedTimespan }); setData(response.data); - setCurrentPage(1); // Reset page when timespan changes + setCurrentPage(1); } catch (error) { console.error("Error:", error); setData([]); From a320c04b92529d00369af4a7db00ccc3c39d20b3 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:09:36 +0200 Subject: [PATCH 30/55] Update Uptime Pagination --- app/api/applications/uptime/route.ts | 131 ++++++++------- app/dashboard/uptime/Uptime.tsx | 243 ++++++++++++++++----------- 2 files changed, 216 insertions(+), 158 deletions(-) diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts index 4f7510e..af72649 100644 --- a/app/api/applications/uptime/route.ts +++ b/app/api/applications/uptime/route.ts @@ -2,8 +2,10 @@ import { NextResponse, NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; interface RequestBody { - timespan?: number; -} + timespan?: number; + page?: number; + } + const getTimeRange = (timespan: number) => { const now = new Date(); @@ -84,62 +86,77 @@ const getIntervalKey = (date: Date, timespan: number) => { }; export async function POST(request: NextRequest) { - try { - const { timespan = 1 }: RequestBody = await request.json(); - const { start } = getTimeRange(timespan); - - const applications = await prisma.application.findMany(); - const uptimeHistory = await prisma.uptime_history.findMany({ - where: { - applicationId: { in: applications.map(app => app.id) }, - createdAt: { gte: start } - }, - orderBy: { createdAt: "desc" } - }); - - const intervals = generateIntervals(timespan); - - const result = applications.map(app => { - const appChecks = uptimeHistory.filter(check => check.applicationId === app.id); - const checksMap = new Map(); - - for (const check of appChecks) { - const intervalKey = getIntervalKey(check.createdAt, timespan); - const current = checksMap.get(intervalKey) || { failed: 0, total: 0 }; - current.total++; - if (!check.online) current.failed++; - checksMap.set(intervalKey, current); - } - - const uptimeSummary = intervals.map(interval => { - const intervalKey = getIntervalKey(interval, timespan); - const stats = checksMap.get(intervalKey); - - if (!stats) { - return { - timestamp: interval.toISOString(), - missing: true, - online: null - }; + 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(); + + 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 < 3 : null + }; + }); + return { - timestamp: intervalKey, - missing: false, - online: stats.failed < 3 + appName: app.name, + appId: app.id, + uptimeSummary }; }); - - return { - appName: app.name, - appId: app.id, - uptimeSummary - }; - }); - - return NextResponse.json(result); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : "Unknown error"; - return NextResponse.json({ error: message }, { status: 500 }); - } -} \ No newline at end of file + + 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 }); + } + } \ No newline at end of file diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index b00a325..01b5842 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -63,41 +63,62 @@ interface UptimeData { }[]; } +interface PaginationData { + currentPage: number; + totalPages: number; + totalItems: number; +} + export default function Uptime() { const [data, setData] = useState([]); const [timespan, setTimespan] = useState<1 | 2 | 3>(1); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 5; + const [pagination, setPagination] = useState({ + currentPage: 1, + totalPages: 1, + totalItems: 0 + }); + const [isLoading, setIsLoading] = useState(false); - const maxPage = Math.ceil(data.length / itemsPerPage); - const paginatedData = data.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage - ); - - const getData = async (selectedTimespan: number) => { + const getData = async (selectedTimespan: number, page: number) => { + setIsLoading(true); try { - const response = await axios.post("/api/applications/uptime", { - timespan: selectedTimespan + const response = await axios.post<{ + data: UptimeData[]; + pagination: PaginationData; + }>("/api/applications/uptime", { + timespan: selectedTimespan, + page }); - setData(response.data); - setCurrentPage(1); + + 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 = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); + const newPage = Math.max(1, pagination.currentPage - 1); + setPagination(prev => ({...prev, currentPage: newPage})); + getData(timespan, newPage); }; const handleNext = () => { - setCurrentPage((prev) => Math.min(maxPage, prev + 1)); + const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1); + setPagination(prev => ({...prev, currentPage: newPage})); + getData(timespan, newPage); }; useEffect(() => { - getData(timespan); + getData(timespan, 1); }, [timespan]); return ( @@ -130,7 +151,11 @@ export default function Uptime() { Uptime
-
- {paginatedData.map((app) => { - const reversedSummary = [...app.uptimeSummary].reverse(); - const startTime = reversedSummary[0]?.timestamp; - const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; - return ( - - -
-
- {app.appName} -
- -
-
- {startTime ? timeFormats[timespan](startTime) : ""} - {endTime ? timeFormats[timespan](endTime) : ""} +
+ {isLoading ? ( +
Loading...
+ ) : ( + data.map((app) => { + const reversedSummary = [...app.uptimeSummary].reverse(); + const startTime = reversedSummary[0]?.timestamp; + const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp; + + return ( + + +
+
+ {app.appName}
- -
- {reversedSummary.map((entry) => ( - - -
- - - -
-

- {timespan === 2 ? ( - timeFormats[2](entry.timestamp) - ) : ( - new Date(entry.timestamp).toLocaleString([], { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: timespan === 3 ? undefined : '2-digit', - hour12: false - }) - )} -

-

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

-
- -
-
- - ))} +
+
+ {startTime ? timeFormats[timespan](startTime) : ""} + {endTime ? timeFormats[timespan](endTime) : ""}
- + + +
+ {reversedSummary.map((entry) => ( + + +
+ + + +
+

+ {timespan === 2 ? ( + timeFormats[2](entry.timestamp) + ) : ( + new Date(entry.timestamp).toLocaleString([], { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: timespan === 3 ? undefined : '2-digit', + hour12: false + }) + )} +

+

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

+
+ +
+
+ + ))} +
+ +
-
- - - ); - })} + + + ); + }) + )}
- - {data.length > 0 && ( + + {pagination.totalItems > 0 && !isLoading && (
- Page {currentPage} of {maxPage} + Page {pagination.currentPage} of {pagination.totalPages} +
+ Showing {data.length} of {pagination.totalItems} applications +
)}
From 1088806921883b49b12364b995db110334ded7c6 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:10:16 +0200 Subject: [PATCH 31/55] Delete Uptime History from deleted Applications --- app/api/applications/delete/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/applications/delete/route.ts b/app/api/applications/delete/route.ts index e6bfe2f..a3132f3 100644 --- a/app/api/applications/delete/route.ts +++ b/app/api/applications/delete/route.ts @@ -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 }); From a3b47bd314dcd34e3ecf7b7a905b991f696f82b8 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:21:51 +0200 Subject: [PATCH 32/55] responsive Uptime Page --- app/dashboard/uptime/Uptime.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/dashboard/uptime/Uptime.tsx b/app/dashboard/uptime/Uptime.tsx index 01b5842..8f1ca30 100644 --- a/app/dashboard/uptime/Uptime.tsx +++ b/app/dashboard/uptime/Uptime.tsx @@ -47,10 +47,10 @@ const timeFormats = { }) }; -const gridColumns = { - 1: 30, - 2: 56, - 3: 30 +const minBoxWidths = { + 1: 24, + 2: 24, + 3: 24 }; interface UptimeData { @@ -193,10 +193,9 @@ export default function Uptime() {
{reversedSummary.map((entry) => ( From 871ab74576f9746da027b8765e562e746257a345 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:30:29 +0200 Subject: [PATCH 33/55] Update online status calculation to check if failure rate is below 50% --- app/api/applications/uptime/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts index af72649..e9dfc67 100644 --- a/app/api/applications/uptime/route.ts +++ b/app/api/applications/uptime/route.ts @@ -136,7 +136,7 @@ export async function POST(request: NextRequest) { return { timestamp: intervalKey, missing: !stats, - online: stats ? stats.failed < 3 : null + online: stats ? (stats.failed / stats.total) <= 0.5 : null }; }); From 9c9b556124a0aff45dbc3244e16afe4ec337f9fa Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:49:07 +0200 Subject: [PATCH 34/55] Dashboard Update --- app/dashboard/Dashboard.tsx | 131 +++++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 45 deletions(-) diff --git a/app/dashboard/Dashboard.tsx b/app/dashboard/Dashboard.tsx index ef5fc5b..30e29cf 100644 --- a/app/dashboard/Dashboard.tsx +++ b/app/dashboard/Dashboard.tsx @@ -15,13 +15,17 @@ import { } from "@/components/ui/sidebar"; import { useEffect, useState } from "react"; import axios from "axios"; // Korrekter Import -import { Card, CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Activity, Layers, Network, Server } from "lucide-react"; +import { Button } from "@/components/ui/button"; interface StatsResponse { serverCount: number; applicationCount: number; onlineApplicationsCount: number; } +import Link from "next/link"; + export default function Dashboard() { const [serverCount, setServerCount] = useState(0); @@ -67,50 +71,87 @@ export default function Dashboard() {
-
- - -
-
- {serverCount} - Servers -
-
-
-
- - -
-
- {applicationCount} - Applications -
-
-
-
- - -
-
- - {onlineApplicationsCount}/{applicationCount} - - Applications are online -
-
-
-
-
-
- COMING SOON -
-
-
- COMING SOON -
-
+

Dashboard

+ +
+ + +
+ Servers + +
+ Manage your server infrastructure +
+ +
{serverCount}
+

Active servers

+
+ + + +
+ + + +
+ Applications + +
+ Manage your deployed applications +
+ +
{applicationCount}
+

Running applications

+
+ + + +
+ + + +
+ Uptime + +
+ Monitor your service availability +
+ +
{onlineApplicationsCount}/{applicationCount}
+

online Applications

+
+ + + +
+ + + +
+ Network + +
+ Manage network configuration +
+ +
{serverCount + applicationCount}
+

Active connections

+
+ + + +
+
- ); -} \ No newline at end of file + ) +} From 8059b0e5415b1bd357064fff563adaf1c3cea136 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 14:51:57 +0200 Subject: [PATCH 35/55] Read.md Update & Cover Image --- README.md | 3 +-- public/cover.png | Bin 0 -> 41939 bytes 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 public/cover.png diff --git a/README.md b/README.md index fb6a650..8cf1e8f 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,8 @@ Network Page: ## Roadmap - [X] Edit Applications, Applications searchbar -- [ ] Customizable Dashboard -- [ ] Notifications - [X] Uptime History +- [ ] Notifications - [ ] Simple Server Monitoring - [ ] Improved Network Flowchart with custom elements (like Network switches) - [ ] Advanced Settings (Disable Uptime Tracking & more) diff --git a/public/cover.png b/public/cover.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b7b402b28e071d90209f0e17b9445fb2d65b46 GIT binary patch literal 41939 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 Date: Tue, 15 Apr 2025 14:59:23 +0200 Subject: [PATCH 36/55] Refactor login page header for improved layout --- app/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 608d110..606ceb4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,7 +23,6 @@ import Cookies from "js-cookie"; import { useRouter } from "next/navigation"; import axios from "axios"; - export default function Home() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -56,6 +55,9 @@ export default function Home() { return (
+
+

CoreControl

+
Login From b0d7271d348fa0e715207b9dfd569bf2c37d772e Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:01:11 +0200 Subject: [PATCH 37/55] Enhance login page card width for improved responsiveness --- app/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 606ceb4..f1f67f7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -56,9 +56,9 @@ export default function Home() { return (
-

CoreControl

+

CoreControl

- + Login From c02b057f8ee65240891b20a6bf5048fa376e7438 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:04:07 +0200 Subject: [PATCH 38/55] Readme.md Update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8cf1e8f..eac8044 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ 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/tp1shBTh/image.png) Dashboard Page: -![Dashboard Page](https://i.ibb.co/G3FW5mVX/image.png) +![Dashboard Page](https://i.ibb.co/ymCSQrXZ/image.png) Servers Page: ![Servers Page](https://i.ibb.co/v6Z79wJY/image.png) From 879ce1e6f1c9468142494886c398db7d122e0542 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:05:01 +0200 Subject: [PATCH 39/55] Language issue --- app/dashboard/servers/Servers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index 00b4014..8dcfc20 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -519,7 +519,7 @@ export default function Dashboard() {
- IP: {server.ip || "Nicht angegeben"} + IP: {server.ip || "Not set"}
From b6e17e5d3948ba81583572d7c75334329efba606 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:08:42 +0200 Subject: [PATCH 40/55] Updated Screenshots in Readme.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eac8044..6cfa569 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,19 @@ Dashboard Page: ![Dashboard Page](https://i.ibb.co/ymCSQrXZ/image.png) Servers Page: -![Servers Page](https://i.ibb.co/v6Z79wJY/image.png) +![Servers Page](https://i.ibb.co/dsvHXrPw/image.png) Applications Page: -![Applications Page](https://i.ibb.co/zC1f6s9/image.png) +![Applications Page](https://i.ibb.co/HT8M6pJ0/image.png) + +Uptime Page: +![Uptime Page](https://i.ibb.co/q3JQKn3z/image.png) Network Page: -![Network Page](https://i.ibb.co/XkKYrGQX/image.png) +![Network Page](https://i.ibb.co/Y4SCqsZD/image.png) + +Settings Page: +![Settings Page](https://i.ibb.co/23bv8CR0/image.png) ## Roadmap - [X] Edit Applications, Applications searchbar From aef0d6f8125db90e0e5a41fcd01e87e4155bf8b2 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:11:03 +0200 Subject: [PATCH 41/55] Add default login credentials to README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6cfa569..c800467 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,10 @@ volumes: postgres_data: ``` +**Default Login** +__E-Mail:__ admin@example.com +__Password:__ admin + ## Tech Stack & Credits The application is build with: From a055d722462f56e9d1b24193289e130e67679e8c Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 15:11:55 +0200 Subject: [PATCH 42/55] Fix formatting of default login credentials in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c800467..e675987 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ volumes: postgres_data: ``` -**Default Login** -__E-Mail:__ admin@example.com +**Default Login**\ +__E-Mail:__ admin@example.com\ __Password:__ admin ## Tech Stack & Credits From 39ba85dcf456fd174fcdcedb9b490e9ee868cf0f Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:04:06 +0200 Subject: [PATCH 43/55] Clarify JWT_SECRET placeholder in configuration files --- README.md | 2 +- compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e675987..a526c45 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ services: ports: - "3000:3000" environment: - JWT_SECRET: RANDOM_SECRET + JWT_SECRET: RANDOM_SECRET # Replace with a secure random string DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db diff --git a/compose.yml b/compose.yml index 7475da7..7f61602 100644 --- a/compose.yml +++ b/compose.yml @@ -4,7 +4,7 @@ services: ports: - "3000:3000" environment: - JWT_SECRET: RANDOM_SECRET + JWT_SECRET: RANDOM_SECRET # Replace with a secure random string DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" depends_on: - db From 1e8682646d229da9d98b1484158a4edbd8e674d2 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:11:31 +0200 Subject: [PATCH 44/55] Dashboard Styling Update --- app/dashboard/Dashboard.tsx | 158 +++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/app/dashboard/Dashboard.tsx b/app/dashboard/Dashboard.tsx index 30e29cf..b096e62 100644 --- a/app/dashboard/Dashboard.tsx +++ b/app/dashboard/Dashboard.tsx @@ -1,66 +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 +} 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 { Activity, Layers, Network, Server } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button" interface StatsResponse { - serverCount: number; - applicationCount: number; - onlineApplicationsCount: number; + serverCount: number + applicationCount: number + onlineApplicationsCount: number } -import Link from "next/link"; - export default function Dashboard() { - const [serverCount, setServerCount] = useState(0); - const [applicationCount, setApplicationCount] = useState(0); - const [onlineApplicationsCount, setOnlineApplicationsCount] = useState(0); + const [serverCount, setServerCount] = useState(0) + const [applicationCount, setApplicationCount] = useState(0) + const [onlineApplicationsCount, setOnlineApplicationsCount] = useState(0) const getStats = async () => { try { - const response = await axios.post('/api/dashboard/get', {}); - setServerCount(response.data.serverCount); - setApplicationCount(response.data.applicationCount); - setOnlineApplicationsCount(response.data.onlineApplicationsCount); + const response = await axios.post("/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 ( -
+
- - / - + / @@ -70,87 +65,104 @@ export default function Dashboard() {
-
-

Dashboard

-
+
+

Dashboard

-
- - +
+ +
- Servers - + Servers +
Manage your server infrastructure
- -
{serverCount}
-

Active servers

+ +
{serverCount}
+

Active servers

- -
- - + +
- Applications - + Applications +
Manage your deployed applications
- -
{applicationCount}
-

Running applications

+ +
{applicationCount}
+

Running applications

- -
- - + +
- Uptime - + Uptime +
Monitor your service availability
- -
{onlineApplicationsCount}/{applicationCount}
-

online Applications

+ +
+
+ + {onlineApplicationsCount}/{applicationCount} + +
+ {applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}% +
+
+
+
0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`, + }} + >
+
+

Online applications

+
- -
- - + +
- Network - + Network +
Manage network configuration
- -
{serverCount + applicationCount}
-

Active connections

+ +
{serverCount + applicationCount}
+

Active connections

- -
+
) From 724a63498501618b61991db8782411628766db32 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:14:10 +0200 Subject: [PATCH 45/55] Login page Styling Update --- app/page.tsx | 208 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index f1f67f7..e03f7b5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,110 +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 ( -
-
-

CoreControl

-
- - - Login - - Enter your email and password to login. - - - - {errorVisible && ( - <> -
- - - Error - - {error} - - -
- - )} - -
-
-
- - setUsername(e.target.value)} - /> -
-
-
- -
- setPassword(e.target.value)}/> -
- +
+
+
+
+
+
- - +

CoreControl

+

Sign in to access your dashboard

+
+ + + + Login + Enter your credentials to continue + + + + {errorVisible && ( + + + Authentication Error + {error} + + )} + +
+
+ +
+ + setUsername(e.target.value)} + onKeyDown={handleKeyDown} + required + /> +
+
+ +
+
+ +
+
+ + setPassword(e.target.value)} + onKeyDown={handleKeyDown} + required + /> +
+
+
+
+ + + + +
+
) } From 7eafdef288cc7859c537e96c4597d7db0b550de9 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:18:35 +0200 Subject: [PATCH 46/55] Settings Page Style Update --- app/dashboard/settings/Settings.tsx | 263 ++++++++++++++-------------- 1 file changed, 134 insertions(+), 129 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 7c7a7c7..92efe3d 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -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, @@ -33,7 +33,7 @@ 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 } from "lucide-react"; +import { AlertCircle, Check, Palette, User } from "lucide-react"; export default function Settings() { const { theme, setTheme } = useTheme(); @@ -157,136 +157,141 @@ export default function Settings() {
- Settings -
- - - - - User - -
Manage your user settings here. You can change your email, password, and other account settings.
-
-
-
Change Email
- { emailErrorVisible && -
- - - Error - - {emailError} - - -
- } - { emailSuccess && -
- - - Success - - Email changed successfully. - - -
- } - setEmail(e.target.value)} - className="mb-2" - /> - -
-
-
Change Password
- { passwordErrorVisible && -
- - - Error - - {passwordError} - - -
- } - { passwordSuccess && -
- - - Success - - Password changed successfully. - - -
- } - setOldPassword(e.target.value)} - className="mb-2" - /> - setPassword(e.target.value)} - className="mb-2" - /> - setConfirmPassword(e.target.value)} - className="mb-2" - /> - -
-
-
-
- - Theme - -
Select a theme for the application. You can choose between light, dark, or system theme.
- -
-
-
+
+ Settings +
+
+ + +
+ +

User Settings

+
+ +
+ Manage your user settings here. You can change your email, password, and other account settings. +
+ +
+
+
+

Change Email

+
+ + {emailErrorVisible && ( + + + Error + {emailError} + + )} + + {emailSuccess && ( + + + Success + Email changed successfully. + + )} + +
+ setEmail(e.target.value)} + className="h-11" + /> + +
+
+ +
+
+

Change Password

+
+ + {passwordErrorVisible && ( + + + Error + {passwordError} + + )} + + {passwordSuccess && ( + + + Success + Password changed successfully. + + )} + +
+ setOldPassword(e.target.value)} + className="h-11" + /> + setPassword(e.target.value)} + className="h-11" + /> + setConfirmPassword(e.target.value)} + className="h-11" + /> + +
+
+
+
+
+ + + +
+ +

Theme Settings

+
+
+ +
+ Select a theme for the application. You can choose between light, dark, or system theme. +
+ +
+ +
+
- ); -} \ No newline at end of file + ) +} From 8ff112e86e71e3fb76cac96a75442ea55c26d045 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:19:18 +0200 Subject: [PATCH 47/55] Update padding in CardContent for User and Theme Settings sections --- app/dashboard/settings/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 92efe3d..5b9488e 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -168,7 +168,7 @@ export default function Settings() {

User Settings

- +
Manage your user settings here. You can change your email, password, and other account settings.
@@ -268,7 +268,7 @@ export default function Settings() {

Theme Settings

- +
Select a theme for the application. You can choose between light, dark, or system theme.
From 7660f8bb5cd7fd9331c63194780e073713ec77fb Mon Sep 17 00:00:00 2001 From: headlessdev Date: Tue, 15 Apr 2025 16:25:01 +0200 Subject: [PATCH 48/55] Enhance header styling and increase font size for better visibility across multiple dashboard components --- app/dashboard/applications/Applications.tsx | 6 +++--- app/dashboard/network/Networks.tsx | 2 +- app/dashboard/servers/Servers.tsx | 8 ++++---- app/dashboard/settings/Settings.tsx | 6 +++--- app/dashboard/uptime/Uptime.tsx | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 3cd4199..f21e8cc 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -260,7 +260,7 @@ export default function Dashboard() { -
+
@@ -281,9 +281,9 @@ export default function Dashboard() {
-
+
- Your Applications + Your Applications