v0.0.7->main

v0.0.7
This commit is contained in:
headlessdev 2025-04-19 16:34:14 +02:00 committed by GitHub
commit 63e8744e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1776 additions and 1186 deletions

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/x509" "crypto/x509"
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -40,6 +42,10 @@ type Notification struct {
TelegramChatID sql.NullString TelegramChatID sql.NullString
TelegramToken sql.NullString TelegramToken sql.NullString
DiscordWebhook sql.NullString DiscordWebhook sql.NullString
GotifyUrl sql.NullString
GotifyToken sql.NullString
NtfyUrl sql.NullString
NtfyToken sql.NullString
} }
var ( var (
@ -134,7 +140,7 @@ func isIPAddress(host string) bool {
func loadNotifications(db *sql.DB) ([]Notification, error) { func loadNotifications(db *sql.DB) ([]Notification, error) {
rows, err := db.Query( rows, err := db.Query(
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
"telegramChatId", "telegramToken", "discordWebhook" "telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken"
FROM notification FROM notification
WHERE enabled = true`, WHERE enabled = true`,
) )
@ -149,7 +155,7 @@ func loadNotifications(db *sql.DB) ([]Notification, error) {
if err := rows.Scan( if err := rows.Scan(
&n.ID, &n.Enabled, &n.Type, &n.ID, &n.Enabled, &n.Type,
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo, &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 { ); err != nil {
fmt.Printf("Error scanning notification: %v\n", err) fmt.Printf("Error scanning notification: %v\n", err)
continue continue
@ -200,7 +206,7 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
var notificationTemplate string var notificationTemplate string
err := db.QueryRow("SELECT notification_text FROM settings LIMIT 1").Scan(&notificationTemplate) err := db.QueryRow("SELECT notification_text FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" { if err != nil || notificationTemplate == "" {
notificationTemplate = "The application '!name' (!url) went !status!" notificationTemplate = "The application !name (!url) went !status!"
} }
for _, app := range apps { for _, app := range apps {
@ -314,6 +320,14 @@ func sendNotifications(message string) {
if n.DiscordWebhook.Valid { if n.DiscordWebhook.Valid {
sendDiscord(n, message) 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() 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)
}
}

View File

@ -3,7 +3,19 @@ import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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(); const applicationCount = await prisma.application.count();
@ -12,7 +24,8 @@ export async function POST(request: NextRequest) {
}); });
return NextResponse.json({ return NextResponse.json({
serverCount, serverCountNoVMs,
serverCountOnlyVMs,
applicationCount, applicationCount,
onlineApplicationsCount onlineApplicationsCount
}); });

View File

@ -30,6 +30,8 @@ interface Server {
id: number; id: number;
name: string; name: string;
ip: string; ip: string;
host: boolean;
hostServer: number | null;
} }
interface Application { interface Application {
@ -43,11 +45,15 @@ const NODE_WIDTH = 220;
const NODE_HEIGHT = 60; const NODE_HEIGHT = 60;
const APP_NODE_WIDTH = 160; const APP_NODE_WIDTH = 160;
const APP_NODE_HEIGHT = 40; const APP_NODE_HEIGHT = 40;
const HORIZONTAL_SPACING = 280; const HORIZONTAL_SPACING = 700;
const VERTICAL_SPACING = 60; const VERTICAL_SPACING = 80;
const START_Y = 120; const START_Y = 120;
const ROOT_NODE_WIDTH = 300; const ROOT_NODE_WIDTH = 300;
const CONTAINER_PADDING = 40; 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() { export async function GET() {
try { try {
@ -60,30 +66,13 @@ export async function GET() {
}) as Promise<Application[]>, }) as Promise<Application[]>,
]); ]);
// Root Node // Level 2: Physical Servers
const rootNode: Node = { const serverNodes: Node[] = servers
id: "root", .filter(server => !server.hostServer)
type: "infrastructure", .map((server, index, filteredServers) => {
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",
},
};
// Server Nodes
const serverNodes: Node[] = servers.map((server, index) => {
const xPos = const xPos =
index * HORIZONTAL_SPACING - index * HORIZONTAL_SPACING -
((servers.length - 1) * HORIZONTAL_SPACING) / 2; ((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2;
return { return {
id: `server-${server.id}`, id: `server-${server.id}`,
@ -108,26 +97,107 @@ export async function GET() {
}; };
}); });
// Application Nodes // Level 3: Services and VMs
const appNodes: Node[] = []; const serviceNodes: Node[] = [];
const vmNodes: Node[] = [];
servers.forEach((server) => { servers.forEach((server) => {
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`); const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
const serverX = serverNode?.position.x || 0; if (serverNode) {
const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2; const serverX = serverNode.position.x;
// Services (left column)
applications applications
.filter((app) => app.serverId === server.id) .filter(app => app.serverId === server.id)
.forEach((app, appIndex) => { .forEach((app, appIndex) => {
appNodes.push({ serviceNodes.push({
id: `app-${app.id}`, 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 === vm.data.id)
.forEach((app, appIndex) => {
vmAppNodes.push({
id: `vm-app-${app.id}`,
type: "application", type: "application",
data: { data: {
label: `${app.name}\n${app.localURL}`, label: `${app.name}\n${app.localURL}`,
...app, ...app,
}, },
position: { position: {
x: serverX + xOffset, x: vmX + VM_APP_SPACING,
y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING, y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20),
}, },
style: { style: {
background: "#f5f5f5", background: "#f5f5f5",
@ -145,38 +215,14 @@ export async function GET() {
}); });
}); });
// Connections // Calculate dimensions for root node positioning
const connections: Edge[] = [ const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes];
...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];
let minX = Infinity; let minX = Infinity;
let maxX = -Infinity; let maxX = -Infinity;
let minY = Infinity; let minY = Infinity;
let maxY = -Infinity; let maxY = -Infinity;
allNodes.forEach((node) => { tempNodes.forEach((node) => {
const width = parseInt(node.style.width?.toString() || "0", 10); const width = parseInt(node.style.width?.toString() || "0", 10);
const height = parseInt(node.style.height?.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); 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 = { const containerNode: Node = {
id: 'container', id: 'container',
type: 'container', type: 'container',
data: { label: '' }, data: { label: '' },
position: { position: {
x: minX - CONTAINER_PADDING, x: newMinX - CONTAINER_PADDING,
y: minY - CONTAINER_PADDING y: newMinY - CONTAINER_PADDING
}, },
style: { style: {
width: maxX - minX + 2 * CONTAINER_PADDING, width: newMaxX - newMinX + 2 * CONTAINER_PADDING,
height: maxY - minY + 2 * CONTAINER_PADDING, height: newMaxY - newMinY + 2 * CONTAINER_PADDING,
background: 'transparent', background: 'transparent',
border: '2px dashed #e2e8f0', border: '2px dashed #e2e8f0',
borderRadius: '8px', borderRadius: '8px',
@ -207,6 +283,116 @@ export async function GET() {
zIndex: -1, 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<number, Node[]>();
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<number, Node[]>();
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<number, Node[]>();
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({ return NextResponse.json({
nodes: [containerNode, ...allNodes], nodes: [containerNode, ...allNodes],
edges: connections, edges: connections,

View File

@ -13,12 +13,16 @@ interface AddRequest {
telegramToken?: string; telegramToken?: string;
telegramChatId?: string; telegramChatId?: string;
discordWebhook?: string; discordWebhook?: string;
gotifyUrl?: string;
gotifyToken?: string;
ntfyUrl?: string;
ntfyToken?: string;
} }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body: AddRequest = await request.json(); 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({ const notification = await prisma.notification.create({
data: { data: {
@ -33,6 +37,10 @@ export async function POST(request: NextRequest) {
telegramChatId: telegramChatId, telegramChatId: telegramChatId,
telegramToken: telegramToken, telegramToken: telegramToken,
discordWebhook: discordWebhook, discordWebhook: discordWebhook,
gotifyUrl: gotifyUrl,
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
} }
}); });

View File

@ -5,6 +5,7 @@ interface AddRequest {
host: boolean; host: boolean;
hostServer: number; hostServer: number;
name: string; name: string;
icon: string;
os: string; os: string;
ip: string; ip: string;
url: string; url: string;
@ -18,13 +19,14 @@ interface AddRequest {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body: AddRequest = await request.json(); 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({ const server = await prisma.server.create({
data: { data: {
host, host,
hostServer, hostServer,
name, name,
icon,
os, os,
ip, ip,
url, url,

View File

@ -6,6 +6,7 @@ interface EditRequest {
hostServer: number; hostServer: number;
id: number; id: number;
name: string; name: string;
icon: string;
os: string; os: string;
ip: string; ip: string;
url: string; url: string;
@ -18,19 +19,27 @@ interface EditRequest {
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const body: EditRequest = await request.json(); 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 } }); const existingServer = await prisma.server.findUnique({ where: { id } });
if (!existingServer) { if (!existingServer) {
return NextResponse.json({ error: "Server not found" }, { status: 404 }); 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({ const updatedServer = await prisma.server.update({
where: { id }, where: { id },
data: { data: {
host, host,
hostServer, hostServer: newHostServer,
name, name,
icon,
os, os,
ip, ip,
url, url,

View File

@ -20,17 +20,29 @@ export async function POST(request: NextRequest) {
}); });
const hostsWithVms = await Promise.all( const hostsWithVms = await Promise.all(
hosts.map(async (host) => ({ hosts.map(async (host) => {
...host, const vms = await prisma.server.findMany({
hostedVMs: await prisma.server.findMany({
where: { hostServer: host.id }, where: { hostServer: host.id },
orderBy: { name: 'asc' } 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({ const totalHosts = await prisma.server.count({
where: { hostServer: null } where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
}); });
const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);

View File

@ -6,7 +6,15 @@ export async function GET(request: NextRequest) {
const servers = await prisma.server.findMany({ const servers = await prisma.server.findMany({
where: { host: true }, 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) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }

View File

@ -11,15 +11,51 @@ export async function POST(request: NextRequest) {
const body: SearchRequest = await request.json(); const body: SearchRequest = await request.json();
const { searchterm } = body; const { searchterm } = body;
// Fetch all servers
const servers = await prisma.server.findMany({}); 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 = { const fuseOptions = {
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'], keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'],
threshold: 0.3, threshold: 0.3,
includeScore: true, includeScore: true,
}; };
const fuse = new Fuse(servers, fuseOptions); const fuse = new Fuse(serversWithType, fuseOptions);
const searchResults = fuse.search(searchterm); const searchResults = fuse.search(searchterm);

View File

@ -19,20 +19,23 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
interface StatsResponse { interface StatsResponse {
serverCount: number serverCountNoVMs: number
serverCountOnlyVMs: number
applicationCount: number applicationCount: number
onlineApplicationsCount: number onlineApplicationsCount: number
} }
export default function Dashboard() { export default function Dashboard() {
const [serverCount, setServerCount] = useState<number>(0) const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
const [applicationCount, setApplicationCount] = useState<number>(0) const [applicationCount, setApplicationCount] = useState<number>(0)
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0) const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
const getStats = async () => { const getStats = async () => {
try { try {
const response = await axios.post<StatsResponse>("/api/dashboard/get", {}) const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
setServerCount(response.data.serverCount) setServerCountNoVMs(response.data.serverCountNoVMs)
setServerCountOnlyVMs(response.data.serverCountOnlyVMs)
setApplicationCount(response.data.applicationCount) setApplicationCount(response.data.applicationCount)
setOnlineApplicationsCount(response.data.onlineApplicationsCount) setOnlineApplicationsCount(response.data.onlineApplicationsCount)
} catch (error: any) { } catch (error: any) {
@ -69,21 +72,51 @@ export default function Dashboard() {
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1> <h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2"> <div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-sm transition-all hover:shadow-md"> <Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-lg transition-all hover:shadow-xl hover:border-t-rose-600">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Servers</CardTitle> <div>
<Server className="h-6 w-6 text-rose-500" /> <CardTitle className="text-2xl font-semibold">Servers</CardTitle>
<CardDescription className="mt-1">Physical and virtual servers overview</CardDescription>
</div>
<Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" />
</div> </div>
<CardDescription>Manage your server infrastructure</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-2 pb-4"> <CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{serverCount}</div> <div className="grid grid-cols-2 gap-4">
<p className="text-sm text-muted-foreground mt-2">Active servers</p> {/* Physical Servers */}
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
<div className="bg-rose-100 p-2 rounded-full">
<Server className="h-6 w-6 text-rose-600" />
</div>
<div>
<div className="text-3xl font-bold">{serverCountNoVMs}</div>
<p className="text-sm text-muted-foreground">Physical Servers</p>
</div>
</div>
{/* Virtual Machines */}
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
<div className="bg-violet-100 p-2 rounded-full">
<Network className="h-6 w-6 text-violet-600" />
</div>
<div>
<div className="text-3xl font-bold">{serverCountOnlyVMs}</div>
<p className="text-sm text-muted-foreground">Virtual Servers</p>
</div>
</div>
</div>
</CardContent> </CardContent>
<CardFooter className="border-t bg-muted/20 p-4"> <CardFooter className="border-t bg-muted/10 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild> <Button
<Link href="/dashboard/servers">View all servers</Link> variant="ghost"
size="lg"
className="w-full hover:bg-rose-50font-semibold transition-colors"
asChild
>
<Link href="/dashboard/servers" className="flex items-center justify-between">
<span>Manage Servers</span>
</Link>
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@ -152,7 +185,7 @@ export default function Dashboard() {
<CardDescription>Manage network configuration</CardDescription> <CardDescription>Manage network configuration</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-2 pb-4"> <CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{serverCount + applicationCount}</div> <div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active connections</p> <p className="text-sm text-muted-foreground mt-2">Active connections</p>
</CardContent> </CardContent>
<CardFooter className="border-t bg-muted/20 p-4"> <CardFooter className="border-t bg-muted/20 p-4">

View File

@ -479,7 +479,7 @@ export default function Dashboard() {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<div className="flex flex-col items-end justify-start space-y-2 w-[270px]"> <div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex flex-col space-y-2 flex-grow"> <div className="flex flex-col space-y-2 flex-grow">
<Button <Button
@ -490,7 +490,7 @@ export default function Dashboard() {
} }
> >
<Link className="h-4 w-4" /> <Link className="h-4 w-4" />
Open Public URL Public URL
</Button> </Button>
{app.localURL && ( {app.localURL && (
<Button <Button
@ -501,7 +501,7 @@ export default function Dashboard() {
} }
> >
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
Open Local URL Local URL
</Button> </Button>
)} )}
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,25 @@
import { AppSidebar } from "@/components/app-sidebar"; "use client"
import { AppSidebar } from "@/components/app-sidebar"
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator"
import { import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
SidebarInset, import { Card, CardContent, CardHeader } from "@/components/ui/card"
SidebarProvider, import { useTheme } from "next-themes"
SidebarTrigger, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
} 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 { Input } from "@/components/ui/input"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import axios from "axios"; import axios from "axios"
import Cookies from "js-cookie"; import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 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 { import {
AlertDialog, AlertDialog,
@ -39,19 +31,19 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox"
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea"
interface NotificationsResponse { interface NotificationsResponse {
notifications: any[]; notifications: any[]
} }
interface NotificationResponse { interface NotificationResponse {
notification_text?: string; notification_text?: string
} }
export default function Settings() { export default function Settings() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme()
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
const [password, setPassword] = useState<string>("") const [password, setPassword] = useState<string>("")
@ -77,95 +69,98 @@ export default function Settings() {
const [telegramToken, setTelegramToken] = useState<string>("") const [telegramToken, setTelegramToken] = useState<string>("")
const [telegramChatId, setTelegramChatId] = useState<string>("") const [telegramChatId, setTelegramChatId] = useState<string>("")
const [discordWebhook, setDiscordWebhook] = useState<string>("") const [discordWebhook, setDiscordWebhook] = useState<string>("")
const [gotifyUrl, setGotifyUrl] = useState<string>("")
const [gotifyToken, setGotifyToken] = useState<string>("")
const [ntfyUrl, setNtfyUrl] = useState<string>("")
const [ntfyToken, setNtfyToken] = useState<string>("")
const [notifications, setNotifications] = useState<any[]>([]) const [notifications, setNotifications] = useState<any[]>([])
const [notificationText, setNotificationText] = useState<string>("") const [notificationText, setNotificationText] = useState<string>("")
const changeEmail = async () => { const changeEmail = async () => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailSuccess(false); setEmailSuccess(false)
setEmailError(""); setEmailError("")
if (!email) { if (!email) {
setEmailError("Email is required"); setEmailError("Email is required")
setEmailErrorVisible(true); setEmailErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailError(""); setEmailError("")
} }, 3000)
, 3000); return
return;
} }
try { try {
await axios.post('/api/auth/edit_email', { await axios.post("/api/auth/edit_email", {
newEmail: email, newEmail: email,
jwtToken: Cookies.get('token') jwtToken: Cookies.get("token"),
}); })
setEmailSuccess(true); setEmailSuccess(true)
setEmail(""); setEmail("")
setTimeout(() => { setTimeout(() => {
setEmailSuccess(false); setEmailSuccess(false)
}, 3000); }, 3000)
} catch (error: any) { } catch (error: any) {
setEmailError(error.response.data.error); setEmailError(error.response.data.error)
setEmailErrorVisible(true); setEmailErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailError(""); setEmailError("")
}, 3000); }, 3000)
} }
} }
const changePassword = async () => { const changePassword = async () => {
try { try {
if (password !== confirmPassword) { if (password !== confirmPassword) {
setPasswordError("Passwords do not match"); setPasswordError("Passwords do not match")
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
return; return
} }
if (!oldPassword || !password || !confirmPassword) { if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required"); setPasswordError("All fields are required")
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
return; return
} }
const response = await axios.post('/api/auth/edit_password', { const response = await axios.post("/api/auth/edit_password", {
oldPassword: oldPassword, oldPassword: oldPassword,
newPassword: password, newPassword: password,
jwtToken: Cookies.get('token') jwtToken: Cookies.get("token"),
}); })
if (response.status === 200) { if (response.status === 200) {
setPasswordSuccess(true); setPasswordSuccess(true)
setPassword(""); setPassword("")
setOldPassword(""); setOldPassword("")
setConfirmPassword(""); setConfirmPassword("")
setTimeout(() => { setTimeout(() => {
setPasswordSuccess(false); setPasswordSuccess(false)
}, 3000); }, 3000)
} }
} catch (error: any) { } catch (error: any) {
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setPasswordError(error.response.data.error); setPasswordError(error.response.data.error)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
} }
} }
const addNotification = async () => { const addNotification = async () => {
try { try {
const response = await axios.post('/api/notifications/add', { const response = await axios.post("/api/notifications/add", {
type: notificationType, type: notificationType,
smtpHost: smtpHost, smtpHost: smtpHost,
smtpPort: smtpPort, smtpPort: smtpPort,
@ -176,37 +171,39 @@ export default function Settings() {
smtpTo: smtpTo, smtpTo: smtpTo,
telegramToken: telegramToken, telegramToken: telegramToken,
telegramChatId: telegramChatId, telegramChatId: telegramChatId,
discordWebhook: discordWebhook discordWebhook: discordWebhook,
}); gotifyUrl: gotifyUrl,
getNotifications(); gotifyToken: gotifyToken,
} ntfyUrl: ntfyUrl,
catch (error: any) { ntfyToken: ntfyToken,
alert(error.response.data.error); })
getNotifications()
} catch (error: any) {
alert(error.response.data.error)
} }
} }
const deleteNotification = async (id: number) => { const deleteNotification = async (id: number) => {
try { try {
const response = await axios.post('/api/notifications/delete', { const response = await axios.post("/api/notifications/delete", {
id: id id: id,
}); })
if (response.status === 200) { if (response.status === 200) {
getNotifications() getNotifications()
} }
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
} }
} }
const getNotifications = async () => { const getNotifications = async () => {
try { try {
const response = await axios.post<NotificationsResponse>('/api/notifications/get', {}); const response = await axios.post<NotificationsResponse>("/api/notifications/get", {})
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
setNotifications(response.data.notifications); setNotifications(response.data.notifications)
} }
} } catch (error: any) {
catch (error: any) { alert(error.response.data.error)
alert(error.response.data.error);
} }
} }
@ -214,29 +211,28 @@ export default function Settings() {
getNotifications() getNotifications()
}, []) }, [])
const getNotificationText = async () => { const getNotificationText = async () => {
try { try {
const response = await axios.post<NotificationResponse>('/api/settings/get_notification_text', {}); const response = await axios.post<NotificationResponse>("/api/settings/get_notification_text", {})
if (response.status === 200) { if (response.status === 200) {
if (response.data.notification_text) { if (response.data.notification_text) {
setNotificationText(response.data.notification_text); setNotificationText(response.data.notification_text)
} else { } else {
setNotificationText("The application !name (!url) is now !status."); setNotificationText("The application !name (!url) is now !status.")
} }
} }
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
}
} }
};
const editNotificationText = async () => { const editNotificationText = async () => {
try { try {
const response = await axios.post('/api/settings/notification_text', { const response = await axios.post("/api/settings/notification_text", {
text: notificationText text: notificationText,
}); })
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
} }
} }
@ -403,23 +399,25 @@ export default function Settings() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm"> <Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b"> <CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" /> <Bell className="h-5 w-5 text-primary" />
</div>
<h2 className="text-xl font-semibold">Notifications</h2> <h2 className="text-xl font-semibold">Notifications</h2>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pb-6"> <CardContent className="p-6">
<div className="text-sm text-muted-foreground mb-6"> <div className="text-sm text-muted-foreground mb-6">
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.
</div> </div>
<div className="grid gap-4 md:grid-cols-2">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button className="w-full"> <Button className="w-full h-11 flex items-center gap-2">
Add Notification Add Notification Channel
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
@ -433,6 +431,8 @@ export default function Settings() {
<SelectItem value="smtp">SMTP</SelectItem> <SelectItem value="smtp">SMTP</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem> <SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="discord">Discord</SelectItem> <SelectItem value="discord">Discord</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem>
</SelectContent> </SelectContent>
{notificationType === "smtp" && ( {notificationType === "smtp" && (
@ -459,10 +459,7 @@ export default function Settings() {
</div> </div>
<div className="flex items-center space-x-2 pt-2 pb-4"> <div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox <Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
id="smtpSecure"
onCheckedChange={(checked: any) => setSmtpSecure(checked)}
/>
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none"> <Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL) Secure Connection (TLS/SSL)
</Label> </Label>
@ -518,11 +515,21 @@ export default function Settings() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label> <Label htmlFor="telegramToken">Bot Token</Label>
<Input type="text" id="telegramToken" placeholder="" onChange={(e) => setTelegramToken(e.target.value)} /> <Input
type="text"
id="telegramToken"
placeholder=""
onChange={(e) => setTelegramToken(e.target.value)}
/>
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label> <Label htmlFor="telegramChatId">Chat ID</Label>
<Input type="text" id="telegramChatId" placeholder="" onChange={(e) => setTelegramChatId(e.target.value)} /> <Input
type="text"
id="telegramChatId"
placeholder=""
onChange={(e) => setTelegramChatId(e.target.value)}
/>
</div> </div>
</div> </div>
)} )}
@ -531,29 +538,75 @@ export default function Settings() {
<div className="mt-4"> <div className="mt-4">
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label> <Label htmlFor="discordWebhook">Webhook URL</Label>
<Input type="text" id="discordWebhook" placeholder="" onChange={(e) => setDiscordWebhook(e.target.value)} /> <Input
type="text"
id="discordWebhook"
placeholder=""
onChange={(e) => setDiscordWebhook(e.target.value)}
/>
</div> </div>
</div> </div>
)} )}
{notificationType === "gotify" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="gotifyUrl">Gotify URL</Label>
<Input
type="text"
id="gotifyUrl"
placeholder=""
onChange={(e) => setGotifyUrl(e.target.value)}
/>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="gotifyToken">Gotify Token</Label>
<Input
type="text"
id="gotifyToken"
placeholder=""
onChange={(e) => setGotifyToken(e.target.value)}
/>
</div>
</div>
</div>
)}
{notificationType === "ntfy" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
<Input
type="text"
id="ntfyUrl"
placeholder=""
onChange={(e) => setNtfyUrl(e.target.value)}
/>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="ntfyToken">Ntfy Token</Label>
<Input
type="text"
id="ntfyToken"
placeholder=""
onChange={(e) => setNtfyToken(e.target.value)}
/>
</div>
</div>
</div>
)}
</Select> </Select>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}> <AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
Add
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<div className="pt-4 pb-2"> <Button className="w-full h-11" variant="outline">
<Button className="w-full" variant="secondary">
Customize Notification Text Customize Notification Text
</Button> </Button>
</div>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle> <AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
@ -561,52 +614,110 @@ export default function Settings() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="text">Notification Text</Label> <Label htmlFor="text">Notification Text</Label>
<Textarea id="text" placeholder="Type here..." value={notificationText} onChange={(e) => setNotificationText(e.target.value)} rows={4} /> <Textarea
id="text"
placeholder="Type here..."
value={notificationText}
onChange={(e) => setNotificationText(e.target.value)}
rows={4}
/>
</div> </div>
</div> </div>
<div className="pt-4 text-sm text-muted-foreground"> <div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text: You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2"> <ul className="list-disc list-inside space-y-1 pt-2">
<li><strong>!name</strong> - Application name</li> <li>
<li><strong>!url</strong> - Application URL</li> <strong>!name</strong> - Application name
<li><strong>!status</strong> - Application status (online/offline)</li> </li>
<li>
<strong>!url</strong> - Application URL
</li>
<li>
<strong>!status</strong> - Application status (online/offline)
</li>
</ul> </ul>
</div> </div>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}> <AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
Save
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div>
<div className="mt-6 space-y-4"> <div className="mt-8">
<h3 className="text-lg font-medium mb-4">Active Notification Channels</h3>
<div className="space-y-3">
{notifications.length > 0 ? ( {notifications.length > 0 ? (
notifications.map((notification) => ( notifications.map((notification) => (
<div <div
key={notification.id} key={notification.id}
className="flex items-center justify-between p-4 bg-muted/10 rounded-lg border" className="flex items-center justify-between p-4 rounded-lg border bg-card transition-all hover:shadow-sm"
> >
<div className="flex items-center gap-3">
{notification.type === "smtp" && (
<div className="bg-muted/20 p-2 rounded-full">
<AtSign className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "telegram" && (
<div className="bg-muted/20 p-2 rounded-full">
<Send className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "discord" && (
<div className="bg-muted/20 p-2 rounded-full">
<MessageSquare className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "gotify" && (
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "ntfy" && (
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
)}
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3> <h3 className="font-medium capitalize">{notification.type}</h3>
<p className="text-xs text-muted-foreground">
{notification.type === "smtp" && "Email notifications"}
{notification.type === "telegram" && "Telegram bot alerts"}
{notification.type === "discord" && "Discord webhook alerts"}
{notification.type === "gotify" && "Gotify notifications"}
{notification.type === "ntfy" && "Ntfy notifications"}
</p>
</div>
</div> </div>
<Button <Button
variant="destructive" variant="ghost"
size="sm" size="sm"
className="hover:bg-muted/20"
onClick={() => deleteNotification(notification.id)} onClick={() => deleteNotification(notification.id)}
> >
Delete <Trash2 className="h-4 w-4 mr-1" />
Remove
</Button> </Button>
</div> </div>
)) ))
) : ( ) : (
<div className="text-center text-muted-foreground py-6"> <div className="text-center py-12 border rounded-lg bg-muted/5">
No notifications configured <div className="flex justify-center mb-3">
<div className="bg-muted/20 p-3 rounded-full">
<Bell className="h-6 w-6 text-muted-foreground" />
</div>
</div>
<h3 className="text-lg font-medium mb-1">No notifications configured</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
Add a notification channel to get alerted when your applications change status.
</p>
</div> </div>
)} )}
</div> </div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

52
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "corecontrol", "name": "corecontrol",
"version": "0.0.6", "version": "0.0.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "corecontrol", "name": "corecontrol",
"version": "0.0.6", "version": "0.0.7",
"dependencies": { "dependencies": {
"@prisma/client": "^6.6.0", "@prisma/client": "^6.6.0",
"@prisma/extension-accelerate": "^1.3.0", "@prisma/extension-accelerate": "^1.3.0",
@ -17,6 +17,7 @@
"@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.3", "@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-progress": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.7", "@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-separator": "^1.1.3",
@ -1690,6 +1691,53 @@
} }
} }
}, },
"node_modules/@radix-ui/react-progress": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz",
"integrity": "sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0"
},
"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-progress/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"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-roving-focus": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "corecontrol", "name": "corecontrol",
"version": "0.0.6", "version": "0.0.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
@ -18,6 +18,7 @@
"@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.3", "@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-progress": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.7", "@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-separator": "^1.1.3",

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "notification" ADD COLUMN "gotifyToken" TEXT,
ADD COLUMN "gotifyUrl" TEXT,
ADD COLUMN "ntfyToken" TEXT,
ADD COLUMN "ntfyUrl" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "server" ADD COLUMN "icon" TEXT;

View File

@ -37,6 +37,7 @@ model server {
host Boolean @default(false) host Boolean @default(false)
hostServer Int? hostServer Int?
name String name String
icon String?
os String? os String?
ip String? ip String?
url String? url String?
@ -72,4 +73,8 @@ model notification {
telegramChatId String? telegramChatId String?
telegramToken String? telegramToken String?
discordWebhook String? discordWebhook String?
gotifyUrl String?
gotifyToken String?
ntfyUrl String?
ntfyToken String?
} }