diff --git a/agent/main.go b/agent/main.go index 5290bec..48391bd 100644 --- a/agent/main.go +++ b/agent/main.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "context" "crypto/x509" "database/sql" + "encoding/json" "errors" "fmt" "net" @@ -40,6 +42,10 @@ type Notification struct { TelegramChatID sql.NullString TelegramToken sql.NullString DiscordWebhook sql.NullString + GotifyUrl sql.NullString + GotifyToken sql.NullString + NtfyUrl sql.NullString + NtfyToken sql.NullString } var ( @@ -134,7 +140,7 @@ func isIPAddress(host string) bool { func loadNotifications(db *sql.DB) ([]Notification, error) { rows, err := db.Query( `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", - "telegramChatId", "telegramToken", "discordWebhook" + "telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken" FROM notification WHERE enabled = true`, ) @@ -149,7 +155,7 @@ func loadNotifications(db *sql.DB) ([]Notification, error) { if err := rows.Scan( &n.ID, &n.Enabled, &n.Type, &n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo, - &n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, + &n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken, ); err != nil { fmt.Printf("Error scanning notification: %v\n", err) continue @@ -200,7 +206,7 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { var notificationTemplate string err := db.QueryRow("SELECT notification_text FROM settings LIMIT 1").Scan(¬ificationTemplate) if err != nil || notificationTemplate == "" { - notificationTemplate = "The application '!name' (!url) went !status!" + notificationTemplate = "The application !name (!url) went !status!" } for _, app := range apps { @@ -314,6 +320,14 @@ func sendNotifications(message string) { if n.DiscordWebhook.Valid { sendDiscord(n, message) } + case "gotify": + if n.GotifyUrl.Valid && n.GotifyToken.Valid { + sendGotify(n, message) + } + case "ntfy": + if n.NtfyUrl.Valid && n.NtfyToken.Valid { + sendNtfy(n, message) + } } } } @@ -371,3 +385,69 @@ func sendDiscord(n Notification, message string) { } resp.Body.Close() } + +func sendGotify(n Notification, message string) { + baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/") + targetURL := fmt.Sprintf("%s/message", baseURL) + + form := url.Values{} + form.Add("message", message) + form.Add("priority", "5") + + req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode())) + if err != nil { + fmt.Printf("Gotify: ERROR creating request: %v\n", err) + return + } + + req.Header.Set("X-Gotify-Key", n.GotifyToken.String) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Gotify: ERROR sending request: %v\n", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode) + } +} + +func sendNtfy(n Notification, message string) { + baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/") + topic := "corecontrol" + requestURL := fmt.Sprintf("%s/%s", baseURL, topic) + + payload := map[string]string{"message": message} + jsonData, err := json.Marshal(payload) + if err != nil { + fmt.Printf("Ntfy: ERROR marshaling JSON: %v\n", err) + return + } + + req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Printf("Ntfy: ERROR creating request: %v\n", err) + return + } + + if n.NtfyToken.Valid { + req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Ntfy: ERROR sending request: %v\n", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode) + } +} diff --git a/app/api/dashboard/get/route.ts b/app/api/dashboard/get/route.ts index c35c557..c8d6949 100644 --- a/app/api/dashboard/get/route.ts +++ b/app/api/dashboard/get/route.ts @@ -3,7 +3,19 @@ import { prisma } from "@/lib/prisma"; export async function POST(request: NextRequest) { try { - const serverCount = await prisma.server.count(); + const serverCountNoVMs = await prisma.server.count({ + where: { + hostServer: 0 + } + }); + + const serverCountOnlyVMs = await prisma.server.count({ + where: { + hostServer: { + not: 0 + } + } + }); const applicationCount = await prisma.application.count(); @@ -12,7 +24,8 @@ export async function POST(request: NextRequest) { }); return NextResponse.json({ - serverCount, + serverCountNoVMs, + serverCountOnlyVMs, applicationCount, onlineApplicationsCount }); diff --git a/app/api/flowchart/route.ts b/app/api/flowchart/route.ts index 737dbfb..4874d16 100644 --- a/app/api/flowchart/route.ts +++ b/app/api/flowchart/route.ts @@ -30,6 +30,8 @@ interface Server { id: number; name: string; ip: string; + host: boolean; + hostServer: number | null; } interface Application { @@ -43,11 +45,15 @@ 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 = 60; +const HORIZONTAL_SPACING = 700; +const VERTICAL_SPACING = 80; const START_Y = 120; const ROOT_NODE_WIDTH = 300; const CONTAINER_PADDING = 40; +const COLUMN_SPACING = 220; +const VM_APP_SPACING = 220; +const MIN_VM_SPACING = 10; +const APP_ROW_SPACING = 15; export async function GET() { try { @@ -60,74 +66,138 @@ export async function GET() { }) as Promise, ]); - // Root Node - const rootNode: Node = { - id: "root", - type: "infrastructure", - data: { label: "My Infrastructure" }, - position: { x: 0, y: 0 }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "8px", - padding: "16px", - width: ROOT_NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "1.2rem", - fontWeight: "bold", - }, - }; + // Level 2: Physical Servers + const serverNodes: Node[] = servers + .filter(server => !server.hostServer) + .map((server, index, filteredServers) => { + const xPos = + index * HORIZONTAL_SPACING - + ((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2; - // Server Nodes - const serverNodes: Node[] = servers.map((server, index) => { - const xPos = - index * HORIZONTAL_SPACING - - ((servers.length - 1) * HORIZONTAL_SPACING) / 2; + return { + id: `server-${server.id}`, + type: "server", + data: { + label: `${server.name}\n${server.ip}`, + ...server, + }, + position: { x: xPos, y: START_Y }, + style: { + background: "#ffffff", + color: "#0f0f0f", + border: "2px solid #e6e4e1", + borderRadius: "4px", + padding: "8px", + width: NODE_WIDTH, + height: NODE_HEIGHT, + fontSize: "0.9rem", + lineHeight: "1.2", + whiteSpace: "pre-wrap", + }, + }; + }); - return { - id: `server-${server.id}`, - type: "server", - data: { - label: `${server.name}\n${server.ip}`, - ...server, - }, - position: { x: xPos, y: START_Y }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "4px", - padding: "8px", - width: NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "0.9rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", - }, - }; - }); - - // Application Nodes - const appNodes: Node[] = []; + // Level 3: Services and VMs + const serviceNodes: Node[] = []; + const vmNodes: Node[] = []; + servers.forEach((server) => { const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`); - const serverX = serverNode?.position.x || 0; - const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2; + if (serverNode) { + const serverX = serverNode.position.x; + + // Services (left column) + applications + .filter(app => app.serverId === server.id) + .forEach((app, appIndex) => { + serviceNodes.push({ + id: `service-${app.id}`, + type: "service", + data: { + label: `${app.name}\n${app.localURL}`, + ...app, + }, + position: { + x: serverX - COLUMN_SPACING, + y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20), + }, + style: { + background: "#f0f9ff", + color: "#0f0f0f", + border: "2px solid #60a5fa", + borderRadius: "4px", + padding: "6px", + width: APP_NODE_WIDTH, + height: APP_NODE_HEIGHT, + fontSize: "0.8rem", + lineHeight: "1.1", + whiteSpace: "pre-wrap", + }, + }); + }); + // VMs (middle column) mit dynamischem Abstand + const hostVMs = servers.filter(vm => vm.hostServer === server.id); + let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING; + + hostVMs.forEach(vm => { + const appCount = applications.filter(app => app.serverId === vm.id).length; + + vmNodes.push({ + id: `vm-${vm.id}`, + type: "vm", + data: { + label: `${vm.name}\n${vm.ip}`, + ...vm, + }, + position: { + x: serverX, + y: currentY, + }, + style: { + background: "#fef2f2", + color: "#0f0f0f", + border: "2px solid #fecaca", + borderRadius: "4px", + padding: "6px", + width: APP_NODE_WIDTH, + height: APP_NODE_HEIGHT, + fontSize: "0.8rem", + lineHeight: "1.1", + whiteSpace: "pre-wrap", + }, + }); + + // Dynamischer Abstand basierend auf Anzahl Apps + const requiredSpace = appCount > 0 + ? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING)) + : 0; + + currentY += Math.max( + requiredSpace + MIN_VM_SPACING, + MIN_VM_SPACING + APP_NODE_HEIGHT + ); + }); + } + }); + + // Level 4: VM Applications (right column) + const vmAppNodes: Node[] = []; + vmNodes.forEach((vm) => { + const vmX = vm.position.x; applications - .filter((app) => app.serverId === server.id) + .filter(app => app.serverId === vm.data.id) .forEach((app, appIndex) => { - appNodes.push({ - id: `app-${app.id}`, + vmAppNodes.push({ + id: `vm-app-${app.id}`, type: "application", data: { label: `${app.name}\n${app.localURL}`, ...app, }, position: { - x: serverX + xOffset, - y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING, + x: vmX + VM_APP_SPACING, + y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20), }, style: { background: "#f5f5f5", @@ -145,38 +215,14 @@ export async function GET() { }); }); - // Connections - const connections: Edge[] = [ - ...servers.map((server) => ({ - id: `conn-root-${server.id}`, - source: "root", - target: `server-${server.id}`, - type: "straight", - style: { - stroke: "#94a3b8", - strokeWidth: 2, - }, - })), - ...applications.map((app) => ({ - id: `conn-${app.serverId}-${app.id}`, - source: `server-${app.serverId}`, - target: `app-${app.id}`, - type: "straight", - style: { - stroke: "#60a5fa", - strokeWidth: 2, - }, - })), - ]; - - // Container Box - const allNodes = [rootNode, ...serverNodes, ...appNodes]; + // Calculate dimensions for root node positioning + const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes]; let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; - allNodes.forEach((node) => { + tempNodes.forEach((node) => { const width = parseInt(node.style.width?.toString() || "0", 10); const height = parseInt(node.style.height?.toString() || "0", 10); @@ -186,17 +232,47 @@ export async function GET() { maxY = Math.max(maxY, node.position.y + height); }); + const centerX = (minX + maxX) / 2; + const rootX = centerX - ROOT_NODE_WIDTH / 2; + + // Level 1: Root Node (centered at top) + const rootNode: Node = { + id: "root", + type: "infrastructure", + data: { label: "My Infrastructure" }, + position: { x: rootX, y: 0 }, + style: { + background: "#ffffff", + color: "#0f0f0f", + border: "2px solid #e6e4e1", + borderRadius: "8px", + padding: "16px", + width: ROOT_NODE_WIDTH, + height: NODE_HEIGHT, + fontSize: "1.2rem", + fontWeight: "bold", + }, + }; + + // Update dimensions with root node + const allNodes = [rootNode, ...tempNodes]; + let newMinX = Math.min(minX, rootNode.position.x); + let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH); + let newMinY = Math.min(minY, rootNode.position.y); + let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT); + + // Container Node const containerNode: Node = { id: 'container', type: 'container', data: { label: '' }, position: { - x: minX - CONTAINER_PADDING, - y: minY - CONTAINER_PADDING + x: newMinX - CONTAINER_PADDING, + y: newMinY - CONTAINER_PADDING }, style: { - width: maxX - minX + 2 * CONTAINER_PADDING, - height: maxY - minY + 2 * CONTAINER_PADDING, + width: newMaxX - newMinX + 2 * CONTAINER_PADDING, + height: newMaxY - newMinY + 2 * CONTAINER_PADDING, background: 'transparent', border: '2px dashed #e2e8f0', borderRadius: '8px', @@ -207,6 +283,116 @@ export async function GET() { zIndex: -1, }; + // Connections with hierarchical chaining + const connections: Edge[] = []; + + // Root to Servers + serverNodes.forEach((server) => { + connections.push({ + id: `conn-root-${server.id}`, + source: "root", + target: server.id, + type: "straight", + style: { + stroke: "#94a3b8", + strokeWidth: 2, + }, + }); + }); + + // Services chaining + const servicesByServer = new Map(); + serviceNodes.forEach(service => { + const serverId = service.data.serverId; + if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []); + servicesByServer.get(serverId)!.push(service); + }); + servicesByServer.forEach((services, serverId) => { + services.sort((a, b) => a.position.y - b.position.y); + services.forEach((service, index) => { + if (index === 0) { + connections.push({ + id: `conn-service-${service.id}`, + source: `server-${serverId}`, + target: service.id, + type: "straight", + style: { stroke: "#60a5fa", strokeWidth: 2 }, + }); + } else { + const prevService = services[index - 1]; + connections.push({ + id: `conn-service-${service.id}-${prevService.id}`, + source: prevService.id, + target: service.id, + type: "straight", + style: { stroke: "#60a5fa", strokeWidth: 2 }, + }); + } + }); + }); + + // VMs chaining + const vmsByHost = new Map(); + vmNodes.forEach(vm => { + const hostId = vm.data.hostServer; + if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []); + vmsByHost.get(hostId)!.push(vm); + }); + vmsByHost.forEach((vms, hostId) => { + vms.sort((a, b) => a.position.y - b.position.y); + vms.forEach((vm, index) => { + if (index === 0) { + connections.push({ + id: `conn-vm-${vm.id}`, + source: `server-${hostId}`, + target: vm.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } else { + const prevVm = vms[index - 1]; + connections.push({ + id: `conn-vm-${vm.id}-${prevVm.id}`, + source: prevVm.id, + target: vm.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } + }); + }); + + // VM Applications chaining + const appsByVM = new Map(); + vmAppNodes.forEach(app => { + const vmId = app.data.serverId; + if (!appsByVM.has(vmId)) appsByVM.set(vmId, []); + appsByVM.get(vmId)!.push(app); + }); + appsByVM.forEach((apps, vmId) => { + apps.sort((a, b) => a.position.y - b.position.y); + apps.forEach((app, index) => { + if (index === 0) { + connections.push({ + id: `conn-vm-app-${app.id}`, + source: `vm-${vmId}`, + target: app.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } else { + const prevApp = apps[index - 1]; + connections.push({ + id: `conn-vm-app-${app.id}-${prevApp.id}`, + source: prevApp.id, + target: app.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } + }); + }); + return NextResponse.json({ nodes: [containerNode, ...allNodes], edges: connections, diff --git a/app/api/notifications/add/route.ts b/app/api/notifications/add/route.ts index 721bf5a..286bb84 100644 --- a/app/api/notifications/add/route.ts +++ b/app/api/notifications/add/route.ts @@ -13,12 +13,16 @@ interface AddRequest { telegramToken?: string; telegramChatId?: string; discordWebhook?: string; + gotifyUrl?: string; + gotifyToken?: string; + ntfyUrl?: string; + ntfyToken?: string; } export async function POST(request: NextRequest) { try { const body: AddRequest = await request.json(); - const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook } = body; + const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken } = body; const notification = await prisma.notification.create({ data: { @@ -33,6 +37,10 @@ export async function POST(request: NextRequest) { telegramChatId: telegramChatId, telegramToken: telegramToken, discordWebhook: discordWebhook, + gotifyUrl: gotifyUrl, + gotifyToken: gotifyToken, + ntfyUrl: ntfyUrl, + ntfyToken: ntfyToken, } }); diff --git a/app/api/servers/add/route.ts b/app/api/servers/add/route.ts index 34634ce..e29924c 100644 --- a/app/api/servers/add/route.ts +++ b/app/api/servers/add/route.ts @@ -5,6 +5,7 @@ interface AddRequest { host: boolean; hostServer: number; name: string; + icon: string; os: string; ip: string; url: string; @@ -18,13 +19,14 @@ interface AddRequest { export async function POST(request: NextRequest) { try { const body: AddRequest = await request.json(); - const { host, hostServer, name, os, ip, url, cpu, gpu, ram, disk } = body; + const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk } = body; const server = await prisma.server.create({ data: { host, hostServer, name, + icon, os, ip, url, diff --git a/app/api/servers/edit/route.ts b/app/api/servers/edit/route.ts index 6a96261..e0ef2f6 100644 --- a/app/api/servers/edit/route.ts +++ b/app/api/servers/edit/route.ts @@ -6,6 +6,7 @@ interface EditRequest { hostServer: number; id: number; name: string; + icon: string; os: string; ip: string; url: string; @@ -18,19 +19,27 @@ interface EditRequest { export async function PUT(request: NextRequest) { try { const body: EditRequest = await request.json(); - const { host, hostServer, id, name, os, ip, url, cpu, gpu, ram, disk } = body; + const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk } = body; const existingServer = await prisma.server.findUnique({ where: { id } }); if (!existingServer) { return NextResponse.json({ error: "Server not found" }, { status: 404 }); } + let newHostServer = hostServer; + if (hostServer === null) { + newHostServer = 0; + } else { + newHostServer = hostServer; + } + const updatedServer = await prisma.server.update({ where: { id }, data: { host, - hostServer, + hostServer: newHostServer, name, + icon, os, ip, url, diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts index a14ef80..13228c6 100644 --- a/app/api/servers/get/route.ts +++ b/app/api/servers/get/route.ts @@ -20,17 +20,29 @@ export async function POST(request: NextRequest) { }); const hostsWithVms = await Promise.all( - hosts.map(async (host) => ({ - ...host, - hostedVMs: await prisma.server.findMany({ + hosts.map(async (host) => { + const vms = await prisma.server.findMany({ where: { hostServer: host.id }, orderBy: { name: 'asc' } - }) - })) + }); + + // Add isVM flag to VMs + const vmsWithFlag = vms.map(vm => ({ + ...vm, + isVM: true, + hostedVMs: [] // Initialize empty hostedVMs array for VMs + })); + + return { + ...host, + isVM: false, // Mark as physical server/not a VM + hostedVMs: vmsWithFlag + }; + }) ); const totalHosts = await prisma.server.count({ - where: { hostServer: null } + where: { OR: [{ hostServer: 0 }, { hostServer: null }] } }); const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); diff --git a/app/api/servers/hosts/route.ts b/app/api/servers/hosts/route.ts index de4f716..3d62621 100644 --- a/app/api/servers/hosts/route.ts +++ b/app/api/servers/hosts/route.ts @@ -6,7 +6,15 @@ export async function GET(request: NextRequest) { const servers = await prisma.server.findMany({ where: { host: true }, }); - return NextResponse.json({ servers }); + + // Add required properties to ensure consistency + const serversWithProps = servers.map(server => ({ + ...server, + isVM: false, + hostedVMs: [] // Initialize empty hostedVMs array + })); + + return NextResponse.json({ servers: serversWithProps }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } diff --git a/app/api/servers/search/route.ts b/app/api/servers/search/route.ts index 2797331..9266919 100644 --- a/app/api/servers/search/route.ts +++ b/app/api/servers/search/route.ts @@ -11,15 +11,51 @@ export async function POST(request: NextRequest) { const body: SearchRequest = await request.json(); const { searchterm } = body; + // Fetch all servers const servers = await prisma.server.findMany({}); + + // Create a map of host servers with their hosted VMs + const serverMap = new Map(); + servers.forEach(server => { + if (server.host) { + serverMap.set(server.id, { + ...server, + isVM: false, + hostedVMs: [] + }); + } + }); + + // Add VMs to their host servers and mark them as VMs + const serversWithType = servers.map(server => { + // If not a host and has a hostServer, it's a VM + if (!server.host && server.hostServer) { + const hostServer = serverMap.get(server.hostServer); + if (hostServer) { + hostServer.hostedVMs.push({ + ...server, + isVM: true + }); + } + return { + ...server, + isVM: true + }; + } + return { + ...server, + isVM: false, + hostedVMs: serverMap.get(server.id)?.hostedVMs || [] + }; + }); const fuseOptions = { - keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'], + keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'], threshold: 0.3, includeScore: true, }; - const fuse = new Fuse(servers, fuseOptions); + const fuse = new Fuse(serversWithType, fuseOptions); const searchResults = fuse.search(searchterm); diff --git a/app/dashboard/Dashboard.tsx b/app/dashboard/Dashboard.tsx index b096e62..3a38444 100644 --- a/app/dashboard/Dashboard.tsx +++ b/app/dashboard/Dashboard.tsx @@ -19,20 +19,23 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { Button } from "@/components/ui/button" interface StatsResponse { - serverCount: number + serverCountNoVMs: number + serverCountOnlyVMs: number applicationCount: number onlineApplicationsCount: number } export default function Dashboard() { - const [serverCount, setServerCount] = useState(0) + const [serverCountNoVMs, setServerCountNoVMs] = useState(0) + const [serverCountOnlyVMs, setServerCountOnlyVMs] = 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) + setServerCountNoVMs(response.data.serverCountNoVMs) + setServerCountOnlyVMs(response.data.serverCountOnlyVMs) setApplicationCount(response.data.applicationCount) setOnlineApplicationsCount(response.data.onlineApplicationsCount) } catch (error: any) { @@ -69,24 +72,54 @@ export default function Dashboard() {

Dashboard

- - -
- Servers - + + +
+
+ Servers + Physical and virtual servers overview
- Manage your server infrastructure - - -
{serverCount}
-

Active servers

-
- - - - + +
+
+ +
+ {/* Physical Servers */} +
+
+ +
+
+
{serverCountNoVMs}
+

Physical Servers

+
+
+ + {/* Virtual Machines */} +
+
+ +
+
+
{serverCountOnlyVMs}
+

Virtual Servers

+
+
+
+
+ + + +
@@ -152,7 +185,7 @@ export default function Dashboard() { Manage network configuration -
{serverCount + applicationCount}
+
{serverCountNoVMs + serverCountOnlyVMs + applicationCount}

Active connections

diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index f21e8cc..24f1fd2 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -479,7 +479,7 @@ export default function Dashboard() {
-
+
{app.localURL && ( )}
diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index 137db15..3d1077f 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -1,53 +1,40 @@ -"use client"; +"use client" -import { AppSidebar } from "@/components/app-sidebar"; +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 { Button } from "@/components/ui/button"; +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { Button } from "@/components/ui/button" import { Plus, Link, - MonitorCog, + MonitorIcon as MonitorCog, FileDigit, Trash2, LayoutGrid, List, Pencil, Cpu, - Microchip, + MicroscopeIcon as Microchip, MemoryStick, HardDrive, Server, -} from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +} from "lucide-react" +import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Pagination, PaginationContent, - PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, -} from "@/components/ui/pagination"; +} from "@/components/ui/pagination" import { AlertDialog, AlertDialogAction, @@ -58,104 +45,97 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import Cookies from "js-cookie"; -import { useState, useEffect } from "react"; -import axios from "axios"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Alert } from "@/components/ui/alert"; -import { ScrollArea } from "@/components/ui/scroll-area"; +} from "@/components/ui/alert-dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import Cookies from "js-cookie" +import { useState, useEffect } from "react" +import axios from "axios" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Checkbox } from "@/components/ui/checkbox" +import { ScrollArea } from "@/components/ui/scroll-area" +import { DynamicIcon } from "lucide-react/dynamic" interface Server { - id: number; - name: string; - host: boolean; - hostServer: number | null; - os?: string; - ip?: string; - url?: string; - cpu?: string; - gpu?: string; - ram?: string; - disk?: string; - hostedVMs: Server[]; + id: number + name: string + icon: string + host: boolean + hostServer: number | null + os?: string + ip?: string + url?: string + cpu?: string + gpu?: string + ram?: string + disk?: string + hostedVMs?: Server[] + isVM?: boolean } interface GetServersResponse { - servers: Server[]; - maxPage: number; + servers: Server[] + maxPage: number } export default function Dashboard() { - const [host, setHost] = useState(false); - const [hostServer, setHostServer] = useState(0); - const [name, setName] = useState(""); - const [os, setOs] = useState(""); - const [ip, setIp] = useState(""); - const [url, setUrl] = useState(""); - const [cpu, setCpu] = useState(""); - const [gpu, setGpu] = useState(""); - const [ram, setRam] = useState(""); - const [disk, setDisk] = useState(""); + const [host, setHost] = useState(false) + const [hostServer, setHostServer] = useState(0) + const [name, setName] = useState("") + const [icon, setIcon] = useState("") + const [os, setOs] = useState("") + const [ip, setIp] = useState("") + const [url, setUrl] = useState("") + const [cpu, setCpu] = useState("") + const [gpu, setGpu] = useState("") + const [ram, setRam] = useState("") + const [disk, setDisk] = useState("") - 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); + 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) - const [editId, setEditId] = useState(null); - const [editHost, setEditHost] = useState(false); - const [editHostServer, setEditHostServer] = useState(0); - const [editName, setEditName] = useState(""); - const [editOs, setEditOs] = useState(""); - const [editIp, setEditIp] = useState(""); - const [editUrl, setEditUrl] = useState(""); - const [editCpu, setEditCpu] = useState(""); - const [editGpu, setEditGpu] = useState(""); - const [editRam, setEditRam] = useState(""); - const [editDisk, setEditDisk] = useState(""); + const [editId, setEditId] = useState(null) + const [editHost, setEditHost] = useState(false) + const [editHostServer, setEditHostServer] = useState(0) + const [editName, setEditName] = useState("") + const [editIcon, setEditIcon] = useState("") + const [editOs, setEditOs] = useState("") + const [editIp, setEditIp] = useState("") + const [editUrl, setEditUrl] = useState("") + const [editCpu, setEditCpu] = useState("") + const [editGpu, setEditGpu] = useState("") + const [editRam, setEditRam] = useState("") + const [editDisk, setEditDisk] = useState("") - const [searchTerm, setSearchTerm] = useState(""); - const [isSearching, setIsSearching] = useState(false); + const [searchTerm, setSearchTerm] = useState("") + const [isSearching, setIsSearching] = useState(false) - const [hostServers, setHostServers] = useState([]); - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [hostServers, setHostServers] = useState([]) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) useEffect(() => { - const savedLayout = Cookies.get("layoutPreference-servers"); - const layout_bool = savedLayout === "grid"; - setIsGridLayout(layout_bool); - setItemsPerPage(layout_bool ? 6 : 4); - }, []); + const savedLayout = Cookies.get("layoutPreference-servers") + const layout_bool = savedLayout === "grid" + setIsGridLayout(layout_bool) + setItemsPerPage(layout_bool ? 6 : 4) + }, []) const toggleLayout = () => { - const newLayout = !isGridLayout; - setIsGridLayout(newLayout); + const newLayout = !isGridLayout + setIsGridLayout(newLayout) Cookies.set("layoutPreference-servers", newLayout ? "grid" : "standard", { expires: 365, path: "/", sameSite: "strict", - }); - setItemsPerPage(newLayout ? 6 : 4); - }; + }) + setItemsPerPage(newLayout ? 6 : 4) + } const add = async () => { try { @@ -163,6 +143,7 @@ export default function Dashboard() { host, hostServer, name, + icon, os, ip, url, @@ -170,84 +151,82 @@ export default function Dashboard() { gpu, ram, disk, - }); - setIsAddDialogOpen(false); - setHost(false); - setHostServer(0); - - setName(""); - setOs(""); - setIp(""); - setUrl(""); - setCpu(""); - setGpu(""); - setRam(""); - setDisk(""); - getServers(); + }) + setIsAddDialogOpen(false) + setHost(false) + setHostServer(0) + setIcon("") + setName("") + setOs("") + setIp("") + setUrl("") + setCpu("") + setGpu("") + setRam("") + setDisk("") + getServers() } catch (error: any) { - console.log(error.response.data); + console.log(error.response.data) } - }; + } const getServers = async () => { try { - setLoading(true); - const response = await axios.post( - "/api/servers/get", - { - page: currentPage, - ITEMS_PER_PAGE: itemsPerPage, - } - ); + setLoading(true) + const response = await axios.post("/api/servers/get", { + page: currentPage, + ITEMS_PER_PAGE: itemsPerPage, + }) for (const server of response.data.servers) { - console.log("Host Server:" + server.hostServer); - console.log("ID:" + server.id); + console.log("Host Server:" + server.hostServer) + console.log("ID:" + server.id) } - setServers(response.data.servers); - setMaxPage(response.data.maxPage); - setLoading(false); + setServers(response.data.servers) + setMaxPage(response.data.maxPage) + setLoading(false) } catch (error: any) { - console.log(error.response); + console.log(error.response) } - }; + } useEffect(() => { - getServers(); - }, [currentPage, itemsPerPage]); + getServers() + }, [currentPage, itemsPerPage]) const handlePrevious = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; + setCurrentPage((prev) => Math.max(1, prev - 1)) + } const handleNext = () => { - setCurrentPage((prev) => Math.min(maxPage, prev + 1)); - }; + setCurrentPage((prev) => Math.min(maxPage, prev + 1)) + } const deleteApplication = async (id: number) => { try { - await axios.post("/api/servers/delete", { id }); - getServers(); + await axios.post("/api/servers/delete", { id }) + getServers() } catch (error: any) { - console.log(error.response.data); + console.log(error.response.data) } - }; + } const openEditDialog = (server: Server) => { - setEditId(server.id); - setEditHost(server.host); - setEditHostServer(server.hostServer || null); - setEditName(server.name); - setEditOs(server.os || ""); - setEditIp(server.ip || ""); - setEditUrl(server.url || ""); - setEditCpu(server.cpu || ""); - setEditGpu(server.gpu || ""); - setEditRam(server.ram || ""); - setEditDisk(server.disk || ""); - }; + setEditId(server.id) + setEditHost(server.host) + setEditHostServer(server.hostServer || null) + setEditName(server.name) + setEditIcon(server.icon || "") + setEditOs(server.os || "") + setEditIp(server.ip || "") + setEditUrl(server.url || "") + setEditCpu(server.cpu || "") + setEditGpu(server.gpu || "") + setEditRam(server.ram || "") + setEditDisk(server.disk || "") + } const edit = async () => { - if (!editId) return; + if (!editId) return try { await axios.put("/api/servers/edit", { @@ -255,6 +234,7 @@ export default function Dashboard() { host: editHost, hostServer: editHostServer, name: editName, + icon: editIcon, os: editOs, ip: editIp, url: editUrl, @@ -262,57 +242,84 @@ export default function Dashboard() { gpu: editGpu, ram: editRam, disk: editDisk, - }); - getServers(); - setEditId(null); + }) + getServers() + setEditId(null) } catch (error: any) { - console.log(error.response.data); + console.log(error.response.data) } - }; + } const searchServers = async () => { try { - setIsSearching(true); - const response = await axios.post<{ results: Server[] }>( - "/api/servers/search", - { searchterm: searchTerm } - ); - setServers(response.data.results); - setIsSearching(false); + setIsSearching(true) + const response = await axios.post<{ results: Server[] }>("/api/servers/search", { searchterm: searchTerm }) + setServers(response.data.results) + setMaxPage(1) + setIsSearching(false) } catch (error: any) { - console.error("Search error:", error.response?.data); - setIsSearching(false); + console.error("Search error:", error.response?.data) + setIsSearching(false) } - }; + } useEffect(() => { const delayDebounce = setTimeout(() => { if (searchTerm.trim() === "") { - getServers(); + getServers() } else { - searchServers(); + searchServers() } - }, 300); + }, 300) - return () => clearTimeout(delayDebounce); - }, [searchTerm]); + return () => clearTimeout(delayDebounce) + }, [searchTerm]) useEffect(() => { const fetchHostServers = async () => { try { - const response = await axios.get<{ servers: Server[] }>( - "/api/servers/hosts" - ); - setHostServers(response.data.servers); + const response = await axios.get<{ servers: Server[] }>("/api/servers/hosts") + setHostServers(response.data.servers) } catch (error) { - console.error("Error fetching host servers:", error); + console.error("Error fetching host servers:", error) } - }; + } if (isAddDialogOpen || editId !== null) { - fetchHostServers(); + fetchHostServers() } - }, [isAddDialogOpen, editId]); + }, [isAddDialogOpen, editId]) + + // Add this function to get the host server name for a VM + const getHostServerName = (hostServerId: number | null) => { + if (!hostServerId) return "" + const hostServer = servers.find((server) => server.id === hostServerId) + return hostServer ? hostServer.name : "" + } + + const iconCategories = { + Infrastructure: ["server", "network", "database", "cloud", "hard-drive", "router", "wifi", "antenna"], + Computing: ["cpu", "microchip", "memory-stick", "terminal", "code", "binary", "command"], + Monitoring: ["activity", "monitor", "gauge", "bar-chart", "line-chart", "pie-chart"], + Security: ["shield", "lock", "key", "fingerprint", "scan-face"], + Status: ["check-circle", "x-octagon", "alert-triangle", "alarm-check", "life-buoy"], + Other: [ + "settings", + "power", + "folder", + "file-code", + "clipboard-list", + "git-branch", + "git-commit", + "git-merge", + "git-pull-request", + "github", + "bug", + ], + } + + // Flatten icons for search + const allIcons = Object.values(iconCategories).flat() return ( @@ -346,23 +353,11 @@ export default function Dashboard() { - - - {isGridLayout - ? "Switch to list view" - : "Switch to grid view"} - + {isGridLayout ? "Switch to list view" : "Switch to grid view"} @@ -379,12 +374,74 @@ export default function Dashboard() { General Hardware - - Virtualization - + Virtualization
+
+
+ +
+ { + const iconElements = document.querySelectorAll("[data-icon-item]") + const searchTerm = e.target.value.toLowerCase() + + iconElements.forEach((el) => { + const iconName = el.getAttribute("data-icon-name")?.toLowerCase() || "" + if (iconName.includes(searchTerm)) { + ;(el as HTMLElement).style.display = "flex" + } else { + ;(el as HTMLElement).style.display = "none" + } + }) + }} + /> + {Object.entries(iconCategories).map(([category, categoryIcons]) => ( +
+
+ {category} +
+ {categoryIcons.map((iconName) => ( + +
+ + {iconName} +
+
+ ))} +
+ ))} + + +
+
+
+ +
+ {icon && } +
+
+
- Link to a web interface (e.g. Proxmox or - Portainer) with which the server can be + Link to a web interface (e.g. Proxmox or Portainer) with which the server can be managed @@ -459,10 +504,7 @@ export default function Dashboard() {
- setHost(checked === true) - } + onCheckedChange={(checked) => setHost(checked === true)} /> - +
{!host && (
- setEditName(e.target.value) - } - /> -
-
- - -
-
- - - setEditIp(e.target.value) - } - /> -
-
- - - setEditUrl(e.target.value) - } - /> -
-
- - - -
-
- - - setEditCpu(e.target.value) - } - /> -
-
- - - setEditGpu(e.target.value) - } - /> -
-
- - - setEditRam(e.target.value) - } - /> -
-
- - - setEditDisk(e.target.value) - } - /> -
-
-
- -
-
- - setEditHost(checked === true) - } - /> - -
- {!editHost && ( -
- - -
- )} -
-
- - - - - - Cancel - - - - - - - {server.hostedVMs.length > 0 && ( + )} + - - - Hosted VMs - + Edit Server + + + + General + Hardware + Virtualization + + +
+
+
+ +
+ { + const iconElements = + document.querySelectorAll("[data-edit-icon-item]") + const searchTerm = e.target.value.toLowerCase() + + iconElements.forEach((el) => { + const iconName = + el.getAttribute("data-icon-name")?.toLowerCase() || "" + if (iconName.includes(searchTerm)) { + ;(el as HTMLElement).style.display = "flex" + } else { + ;(el as HTMLElement).style.display = "none" + } + }) + }} + /> + {Object.entries(iconCategories).map( + ([category, categoryIcons]) => ( +
+
+ {category} +
+ {categoryIcons.map((iconName) => ( + +
+ + {iconName} +
+
+ ))} +
+ ), + )} + + +
+
+
+ +
+ {editIcon && ( + + )} +
+
+
+
+ + setEditName(e.target.value)} + /> +
+
+ + +
+
+ + setEditIp(e.target.value)} + /> +
+
+ + setEditUrl(e.target.value)} + /> +
+
+
+ + +
+
+ + setEditCpu(e.target.value)} + /> +
+
+ + setEditGpu(e.target.value)} + /> +
+
+ + setEditRam(e.target.value)} + /> +
+
+ + setEditDisk(e.target.value)} + /> +
+
+
+ +
+
+ setEditHost(checked === true)} + disabled={server.hostedVMs && server.hostedVMs.length > 0} + /> + +
+ {!editHost && ( +
+ + +
+ )} +
+
+
+
+
+ + Cancel + + +
+
+
+ + {server.hostedVMs && server.hostedVMs.length > 0 && ( + + + + + + + Hosted VMs {server.host && (
- +
- {server.hostedVMs?.map( - (hostedVM) => ( -
-
-
- {hostedVM.name} -
-
- + {server.hostedVMs?.map((hostedVM) => ( +
+
+
+ {hostedVM.icon && ( + + )} +
{hostedVM.icon && "・ "}{hostedVM.name}
+
+
+ - + - - Edit VM - + Edit VM - + - - General - - - Hardware - + General + Hardware Virtualization
+
+
+ +
+ { + const iconElements = + document.querySelectorAll( + "[data-vm-edit-icon-item]", + ) + const searchTerm = + e.target.value.toLowerCase() + + iconElements.forEach((el) => { + const iconName = + el + .getAttribute( + "data-icon-name", + ) + ?.toLowerCase() || "" + if ( + iconName.includes(searchTerm) + ) { + ;( + el as HTMLElement + ).style.display = "flex" + } else { + ;( + el as HTMLElement + ).style.display = "none" + } + }) + }} + /> + {Object.entries(iconCategories).map( + ([category, categoryIcons]) => ( +
+
+ {category} +
+ {categoryIcons.map((iconName) => ( + +
+ + {iconName} +
+
+ ))} +
+ ), + )} + + +
+
+
+ +
+ {editIcon && ( + + )} +
+
+
- + - setEditName( - e - .target - .value - ) - } + value={editName} + onChange={(e) => setEditName(e.target.value)} />
- +
- + - setEditIp( - e - .target - .value - ) - } + value={editIp} + onChange={(e) => setEditIp(e.target.value)} />
- + - setEditUrl( - e - .target - .value - ) - } + value={editUrl} + onChange={(e) => setEditUrl(e.target.value)} />
@@ -1088,83 +1165,35 @@ export default function Dashboard() {
- + - setEditCpu( - e - .target - .value - ) - } + value={editCpu} + onChange={(e) => setEditCpu(e.target.value)} />
- + - setEditGpu( - e - .target - .value - ) - } + value={editGpu} + onChange={(e) => setEditGpu(e.target.value)} />
- + - setEditRam( - e - .target - .value - ) - } + value={editRam} + onChange={(e) => setEditRam(e.target.value)} />
- + - setEditDisk( - e - .target - .value - ) - } + value={editDisk} + onChange={(e) => setEditDisk(e.target.value)} />
@@ -1174,62 +1203,50 @@ export default function Dashboard() {
+ setEditHost(checked === true) } - onCheckedChange={( - checked - ) => - setEditHost( - checked === - true - ) + disabled={ + server.hostedVMs && + server.hostedVMs.length > 0 } />
{!editHost && (
- +
@@ -1240,73 +1257,59 @@ export default function Dashboard() {
- - Cancel - - + Cancel +
-
- -
- -
-
- - - OS:{" "} - {hostedVM.os || "-"} - -
-
- - - IP:{" "} - {hostedVM.ip || - "Not set"} - -
-
+
+ +
+
- + - CPU:{" "} - {hostedVM.cpu || "-"} + OS: {hostedVM.os || "-"}
- + - GPU:{" "} - {hostedVM.gpu || "-"} - -
-
- - - RAM:{" "} - {hostedVM.ram || "-"} - -
-
- - - Disk:{" "} - {hostedVM.disk || "-"} + IP: {hostedVM.ip || "Not set"}
- ) - )} + +
+ + + CPU: {hostedVM.cpu || "-"} + +
+
+ + + GPU: {hostedVM.gpu || "-"} + +
+
+ + + RAM: {hostedVM.ram || "-"} + +
+
+ + + Disk: {hostedVM.disk || "-"} + +
+
+ ))}
@@ -1314,9 +1317,7 @@ export default function Dashboard() { - - Close - + Close @@ -1338,12 +1339,12 @@ export default function Dashboard() { fill="none" xmlns="http://www.w3.org/2000/svg" > - + @@ -1379,8 +1380,7 @@ export default function Dashboard() { onClick={handleNext} isActive={currentPage < maxPage} style={{ - cursor: - currentPage === maxPage ? "not-allowed" : "pointer", + cursor: currentPage === maxPage ? "not-allowed" : "pointer", }} /> @@ -1390,5 +1390,5 @@ export default function Dashboard() {
- ); + ) } diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 931e2fd..e006d2a 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -1,33 +1,25 @@ -import { AppSidebar } from "@/components/app-sidebar"; +"use client" + +import { AppSidebar } from "@/components/app-sidebar" import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { Separator } from "@/components/ui/separator"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { useTheme } from "next-themes"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { useTheme } from "next-themes" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Input } from "@/components/ui/input" -import { useEffect, useState } from "react"; -import axios from "axios"; -import Cookies from "js-cookie"; -import { Button } from "@/components/ui/button"; +import { useEffect, useState } from "react" +import axios from "axios" +import Cookies from "js-cookie" +import { Button } from "@/components/ui/button" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { AlertCircle, Check, Palette, User, Bell } from "lucide-react"; +import { AlertCircle, Check, Palette, User, Bell, AtSign, Send, MessageSquare, Trash2 } from "lucide-react" import { AlertDialog, @@ -39,19 +31,19 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" -import { Label } from "@/components/ui/label"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label" +import { Checkbox } from "@/components/ui/checkbox" +import { Textarea } from "@/components/ui/textarea" interface NotificationsResponse { - notifications: any[]; + notifications: any[] } interface NotificationResponse { - notification_text?: string; + notification_text?: string } export default function Settings() { - const { theme, setTheme } = useTheme(); + const { theme, setTheme } = useTheme() const [email, setEmail] = useState("") const [password, setPassword] = useState("") @@ -77,95 +69,98 @@ export default function Settings() { const [telegramToken, setTelegramToken] = useState("") const [telegramChatId, setTelegramChatId] = useState("") const [discordWebhook, setDiscordWebhook] = useState("") + const [gotifyUrl, setGotifyUrl] = useState("") + const [gotifyToken, setGotifyToken] = useState("") + const [ntfyUrl, setNtfyUrl] = useState("") + const [ntfyToken, setNtfyToken] = useState("") const [notifications, setNotifications] = useState([]) const [notificationText, setNotificationText] = useState("") const changeEmail = async () => { - setEmailErrorVisible(false); - setEmailSuccess(false); - setEmailError(""); + setEmailErrorVisible(false) + setEmailSuccess(false) + setEmailError("") if (!email) { - setEmailError("Email is required"); - setEmailErrorVisible(true); + setEmailError("Email is required") + setEmailErrorVisible(true) setTimeout(() => { - setEmailErrorVisible(false); - setEmailError(""); - } - , 3000); - return; + setEmailErrorVisible(false) + setEmailError("") + }, 3000) + return } try { - await axios.post('/api/auth/edit_email', { + await axios.post("/api/auth/edit_email", { newEmail: email, - jwtToken: Cookies.get('token') - }); - setEmailSuccess(true); - setEmail(""); + jwtToken: Cookies.get("token"), + }) + setEmailSuccess(true) + setEmail("") setTimeout(() => { - setEmailSuccess(false); - }, 3000); + setEmailSuccess(false) + }, 3000) } catch (error: any) { - setEmailError(error.response.data.error); - setEmailErrorVisible(true); + setEmailError(error.response.data.error) + setEmailErrorVisible(true) setTimeout(() => { - setEmailErrorVisible(false); - setEmailError(""); - }, 3000); + setEmailErrorVisible(false) + setEmailError("") + }, 3000) } } const changePassword = async () => { try { if (password !== confirmPassword) { - setPasswordError("Passwords do not match"); - setPasswordErrorVisible(true); + setPasswordError("Passwords do not match") + setPasswordErrorVisible(true) setTimeout(() => { - setPasswordErrorVisible(false); - setPasswordError(""); - }, 3000); - return; + setPasswordErrorVisible(false) + setPasswordError("") + }, 3000) + return } if (!oldPassword || !password || !confirmPassword) { - setPasswordError("All fields are required"); - setPasswordErrorVisible(true); + setPasswordError("All fields are required") + setPasswordErrorVisible(true) setTimeout(() => { - setPasswordErrorVisible(false); - setPasswordError(""); - }, 3000); - return; + setPasswordErrorVisible(false) + setPasswordError("") + }, 3000) + return } - const response = await axios.post('/api/auth/edit_password', { + const response = await axios.post("/api/auth/edit_password", { oldPassword: oldPassword, newPassword: password, - jwtToken: Cookies.get('token') - }); - + jwtToken: Cookies.get("token"), + }) + if (response.status === 200) { - setPasswordSuccess(true); - setPassword(""); - setOldPassword(""); - setConfirmPassword(""); + setPasswordSuccess(true) + setPassword("") + setOldPassword("") + setConfirmPassword("") setTimeout(() => { - setPasswordSuccess(false); - }, 3000); + setPasswordSuccess(false) + }, 3000) } } catch (error: any) { - setPasswordErrorVisible(true); - setPasswordError(error.response.data.error); + setPasswordErrorVisible(true) + setPasswordError(error.response.data.error) setTimeout(() => { - setPasswordErrorVisible(false); - setPasswordError(""); - }, 3000); + setPasswordErrorVisible(false) + setPasswordError("") + }, 3000) } } const addNotification = async () => { try { - const response = await axios.post('/api/notifications/add', { + const response = await axios.post("/api/notifications/add", { type: notificationType, smtpHost: smtpHost, smtpPort: smtpPort, @@ -176,37 +171,39 @@ export default function Settings() { smtpTo: smtpTo, telegramToken: telegramToken, telegramChatId: telegramChatId, - discordWebhook: discordWebhook - }); - getNotifications(); - } - catch (error: any) { - alert(error.response.data.error); + discordWebhook: discordWebhook, + gotifyUrl: gotifyUrl, + gotifyToken: gotifyToken, + ntfyUrl: ntfyUrl, + ntfyToken: ntfyToken, + }) + getNotifications() + } catch (error: any) { + alert(error.response.data.error) } } const deleteNotification = async (id: number) => { try { - const response = await axios.post('/api/notifications/delete', { - id: id - }); + const response = await axios.post("/api/notifications/delete", { + id: id, + }) if (response.status === 200) { getNotifications() } } catch (error: any) { - alert(error.response.data.error); + alert(error.response.data.error) } } const getNotifications = async () => { try { - const response = await axios.post('/api/notifications/get', {}); + const response = await axios.post("/api/notifications/get", {}) if (response.status === 200 && response.data) { - setNotifications(response.data.notifications); + setNotifications(response.data.notifications) } - } - catch (error: any) { - alert(error.response.data.error); + } catch (error: any) { + alert(error.response.data.error) } } @@ -214,29 +211,28 @@ export default function Settings() { getNotifications() }, []) - const getNotificationText = async () => { try { - const response = await axios.post('/api/settings/get_notification_text', {}); + const response = await axios.post("/api/settings/get_notification_text", {}) if (response.status === 200) { if (response.data.notification_text) { - setNotificationText(response.data.notification_text); + setNotificationText(response.data.notification_text) } else { - setNotificationText("The application !name (!url) is now !status."); + setNotificationText("The application !name (!url) is now !status.") } } } catch (error: any) { - alert(error.response.data.error); + alert(error.response.data.error) } - }; + } const editNotificationText = async () => { try { - const response = await axios.post('/api/settings/notification_text', { - text: notificationText - }); + const response = await axios.post("/api/settings/notification_text", { + text: notificationText, + }) } catch (error: any) { - alert(error.response.data.error); + alert(error.response.data.error) } } @@ -403,209 +399,324 @@ export default function Settings() { - -
- +
+
+ +

Notifications

- +
- Set up Notifications to get notified when an application goes offline or online. + Set up notifications to get instantly alerted when an application changes status.
- - - - - - Add Notification - - setNotificationType(value)}> + + + + + SMTP + Telegram + Discord + Gotify + Ntfy + - {notificationType === "smtp" && ( -
-
-
- - setSmtpHost(e.target.value)} - /> + {notificationType === "smtp" && ( +
+
+
+ + setSmtpHost(e.target.value)} + /> +
+
+ + setSmtpPort(Number(e.target.value))} + /> +
+
+ +
+ setSmtpSecure(checked)} /> + +
+ +
+
+ + setSmtpUsername(e.target.value)} + /> +
+ +
+ + setSmtpPassword(e.target.value)} + /> +
+ +
+
+ + setSmtpFrom(e.target.value)} + /> +
+ +
+ + setSmtpTo(e.target.value)} + /> +
+
+
-
- - setSmtpPort(Number(e.target.value))} - /> -
-
- -
- setSmtpSecure(checked)} - /> - -
- -
-
- - setSmtpUsername(e.target.value)} - /> -
- -
- - setSmtpPassword(e.target.value)} - /> -
- -
-
- + )} + + {notificationType === "telegram" && ( +
+
+ setSmtpFrom(e.target.value)} + type="text" + id="telegramToken" + placeholder="" + onChange={(e) => setTelegramToken(e.target.value)} />
- -
- +
+ setSmtpTo(e.target.value)} + type="text" + id="telegramChatId" + placeholder="" + onChange={(e) => setTelegramChatId(e.target.value)} />
-
-
- )} + )} - {notificationType === "telegram" && ( -
-
- - setTelegramToken(e.target.value)} /> + {notificationType === "discord" && ( +
+
+ + setDiscordWebhook(e.target.value)} + /> +
-
- - setTelegramChatId(e.target.value)} /> + )} + + {notificationType === "gotify" && ( +
+
+ + setGotifyUrl(e.target.value)} + /> +
+ + setGotifyToken(e.target.value)} + /> +
+
-
- )} + )} - {notificationType === "discord" && ( -
-
- - setDiscordWebhook(e.target.value)} /> + {notificationType === "ntfy" && ( +
+
+ + setNtfyUrl(e.target.value)} + /> +
+ + setNtfyToken(e.target.value)} + /> +
+
-
- )} + )} + + + + Cancel + Add + + + - - - - Cancel - - Add - - - - - - - -
- -
-
- - Customize Notification Text - -
+ + + Customize Notification Text + +
-