v0.0.8->main

V0.0.8
This commit is contained in:
headlessdev 2025-04-21 15:51:17 +02:00 committed by GitHub
commit 10acf3c738
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 6662 additions and 247 deletions

View File

@ -2,4 +2,5 @@ node_modules
npm-debug.log
.env
agent/
.next
.next
docs/

6
.gitignore vendored
View File

@ -1,5 +1,11 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Vitepress
docs/.vitepress/dist
docs/.vitepress/cache
docs/node_modules/
# dependencies
/node_modules
/.pnp

View File

@ -20,7 +20,7 @@ Login Page:
![Login Page](https://i.ibb.co/DfS7BJdX/image.png)
Dashboard Page:
![Dashboard Page](https://i.ibb.co/wFMb7StT/image.png)
![Dashboard Page](https://i.ibb.co/mVgGJN6H/image.png)
Servers Page:
![Servers Page](https://i.ibb.co/HLMD9HPZ/image.png)
@ -29,13 +29,13 @@ VM Display:
![VM Display](https://i.ibb.co/My45mv8k/image.png)
Applications Page:
![Applications Page](https://i.ibb.co/qMwrKwn3/image.png)
![Applications Page](https://i.ibb.co/Hc2HQpV/image.png)
Uptime Page:
![Uptime Page](https://i.ibb.co/jvGcL9Y6/image.png)
Network Page:
![Network Page](https://i.ibb.co/qYcL2Fws/image.png)
![Network Page](https://i.ibb.co/ymLHcNqM/image.png)
Settings Page:
![Settings Page](https://i.ibb.co/rRQB9Hcz/image.png)
@ -44,7 +44,8 @@ Settings Page:
- [X] Edit Applications, Applications searchbar
- [X] Uptime History
- [X] Notifications
- [ ] Simple Server Monitoring
- [X] Simple Server Monitoring
- [ ] Simple Server Monitoring History
- [ ] Improved Network Flowchart with custom elements (like Network switches)
- [ ] Advanced Settings (Disable Uptime Tracking & more)
@ -60,9 +61,6 @@ services:
environment:
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
- db
- agent
agent:
image: haedlessdev/corecontrol-agent:latest
@ -105,6 +103,7 @@ The application is build with:
- Icons by [Lucide](https://lucide.dev/)
- Flowcharts by [React Flow](https://reactflow.dev/)
- Application icons by [selfh.st/icons](selfh.st/icons)
- Monitoring Tool by [Glances](https://github.com/nicolargo/glances)
- and a lot of love ❤️
## Star History

View File

@ -28,6 +28,31 @@ type Application struct {
Online bool
}
type Server struct {
ID int
Name string
Monitoring bool
MonitoringURL sql.NullString
Online bool
CpuUsage sql.NullFloat64
RamUsage sql.NullFloat64
DiskUsage sql.NullFloat64
}
type CPUResponse struct {
Total float64 `json:"total"`
}
type MemoryResponse struct {
Percent float64 `json:"percent"`
}
type FSResponse []struct {
DeviceName string `json:"device_name"`
MntPoint string `json:"mnt_point"`
Percent float64 `json:"percent"`
}
type Notification struct {
ID int
Enabled bool
@ -46,6 +71,9 @@ type Notification struct {
GotifyToken sql.NullString
NtfyUrl sql.NullString
NtfyToken sql.NullString
PushoverUrl sql.NullString
PushoverToken sql.NullString
PushoverUser sql.NullString
}
var (
@ -108,20 +136,34 @@ func main() {
}
}()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
client := &http.Client{
appClient := &http.Client{
Timeout: 4 * time.Second,
}
// Server monitoring every 5 seconds
go func() {
serverClient := &http.Client{
Timeout: 5 * time.Second,
}
serverTicker := time.NewTicker(5 * time.Second)
defer serverTicker.Stop()
for range serverTicker.C {
servers := getServers(db)
checkAndUpdateServerStatus(db, serverClient, servers)
}
}()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for now := range ticker.C {
if now.Second()%10 != 0 {
continue
}
apps := getApplications(db)
checkAndUpdateStatus(db, client, apps)
checkAndUpdateStatus(db, appClient, apps)
}
}
@ -169,6 +211,7 @@ func deleteOldEntries(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Delete old uptime history entries
res, err := db.ExecContext(ctx,
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`,
)
@ -177,6 +220,17 @@ func deleteOldEntries(db *sql.DB) error {
}
affected, _ := res.RowsAffected()
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
// Delete old server history entries
res, err = db.ExecContext(ctx,
`DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`,
)
if err != nil {
return err
}
affected, _ = res.RowsAffected()
fmt.Printf("Deleted %d old entries from server_history\n", affected)
return nil
}
@ -202,9 +256,35 @@ func getApplications(db *sql.DB) []Application {
return apps
}
func getServers(db *sql.DB) []Server {
rows, err := db.Query(
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
FROM server WHERE monitoring = true`,
)
if err != nil {
fmt.Printf("Error fetching servers: %v\n", err)
return nil
}
defer rows.Close()
var servers []Server
for rows.Next() {
var server Server
if err := rows.Scan(
&server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL,
&server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage,
); err != nil {
fmt.Printf("Error scanning server row: %v\n", err)
continue
}
servers = append(servers, server)
}
return servers
}
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
var notificationTemplate string
err := db.QueryRow("SELECT notification_text FROM settings LIMIT 1").Scan(&notificationTemplate)
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The application !name (!url) went !status!"
}
@ -301,6 +381,154 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
dbCancel2()
}
}
func checkAndUpdateServerStatus(db *sql.DB, client *http.Client, servers []Server) {
var notificationTemplate string
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The server !name is now !status!"
}
for _, server := range servers {
if !server.Monitoring || !server.MonitoringURL.Valid {
continue
}
logPrefix := fmt.Sprintf("[Server %s]", server.Name)
fmt.Printf("%s Checking...\n", logPrefix)
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
var cpuUsage, ramUsage, diskUsage float64
var online = true
// Get CPU usage
cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL))
if err != nil {
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer cpuResp.Body.Close()
if cpuResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var cpuData CPUResponse
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
cpuUsage = cpuData.Total
}
}
}
if online {
// Get Memory usage
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
if err != nil {
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer memResp.Body.Close()
if memResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var memData MemoryResponse
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
ramUsage = memData.Percent
}
}
}
}
if online {
// Get Disk usage
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
if err != nil {
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer fsResp.Body.Close()
if fsResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var fsData FSResponse
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else if len(fsData) > 0 {
diskUsage = fsData[0].Percent
}
}
}
}
// Check if status changed and send notification if needed
if online != server.Online {
status := "offline"
if online {
status = "online"
}
message := notificationTemplate
message = strings.ReplaceAll(message, "!name", server.Name)
message = strings.ReplaceAll(message, "!status", status)
sendNotifications(message)
}
// Update server status with metrics
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
// Add entry to server history
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = db.ExecContext(ctx,
`INSERT INTO server_history(
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt"
) VALUES ($1, $2, $3, $4, $5, now())`,
server.ID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage),
)
cancel()
if err != nil {
fmt.Printf("%s Failed to insert history: %v\n", logPrefix, err)
}
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%\n",
logPrefix, cpuUsage, ramUsage, diskUsage)
}
}
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.ExecContext(ctx,
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8
WHERE id = $5`,
online, cpuUsage, ramUsage, diskUsage, serverID,
)
if err != nil {
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
}
}
func sendNotifications(message string) {
notifMutex.RLock()
notifs := notifMutexCopy(notifications)
@ -328,6 +556,10 @@ func sendNotifications(message string) {
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
sendNtfy(n, message)
}
case "pushover":
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
sendPushover(n, message)
}
}
}
}
@ -451,3 +683,30 @@ func sendNtfy(n Notification, message string) {
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
}
}
func sendPushover(n Notification, message string) {
form := url.Values{}
form.Add("token", n.PushoverToken.String)
form.Add("user", n.PushoverUser.String)
form.Add("message", message)
req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Pushover: ERROR creating request: %v\n", err)
return
}
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("Pushover: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
}
}

View File

@ -17,12 +17,15 @@ interface AddRequest {
gotifyToken?: string;
ntfyUrl?: string;
ntfyToken?: string;
pushoverUrl?: string;
pushoverToken?: string;
pushoverUser?: 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, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken } = body;
const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser } = body;
const notification = await prisma.notification.create({
data: {
@ -41,6 +44,9 @@ export async function POST(request: NextRequest) {
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken,
pushoverUser: pushoverUser,
}
});

View File

@ -13,13 +13,15 @@ interface AddRequest {
gpu: string;
ram: string;
disk: string;
monitoring: boolean;
monitoringURL: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk } = body;
const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
const server = await prisma.server.create({
data: {
@ -33,7 +35,9 @@ export async function POST(request: NextRequest) {
cpu,
gpu,
ram,
disk
disk,
monitoring,
monitoringURL
}
});

View File

@ -18,6 +18,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
}
// Delete all server history records for this server
await prisma.server_history.deleteMany({
where: { serverId: id }
});
// Delete the server
await prisma.server.delete({
where: { id: id }
});

View File

@ -14,12 +14,14 @@ interface EditRequest {
gpu: string;
ram: string;
disk: string;
monitoring: boolean;
monitoringURL: string;
}
export async function PUT(request: NextRequest) {
try {
const body: EditRequest = await request.json();
const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk } = body;
const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
const existingServer = await prisma.server.findUnique({ where: { id } });
if (!existingServer) {
@ -46,7 +48,9 @@ export async function PUT(request: NextRequest) {
cpu,
gpu,
ram,
disk
disk,
monitoring,
monitoringURL
}
});

View File

@ -0,0 +1,35 @@
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma";
export async function GET() {
try {
const servers = await prisma.server.findMany({
select: {
id: true,
online: true,
cpuUsage: true,
ramUsage: true,
diskUsage: true,
}
});
const monitoringData = servers.map((server: {
id: number;
online: boolean;
cpuUsage: string | null;
ramUsage: string | null;
diskUsage: string | null;
}) => ({
id: server.id,
online: server.online,
cpuUsage: server.cpuUsage ? parseInt(server.cpuUsage) : 0,
ramUsage: server.ramUsage ? parseInt(server.ramUsage) : 0,
diskUsage: server.diskUsage ? parseInt(server.diskUsage) : 0
}));
return NextResponse.json(monitoringData)
} catch (error) {
return new NextResponse("Internal Error", { status: 500 })
}
}

View File

@ -7,7 +7,7 @@ export async function POST(request: NextRequest) {
// Check if there are any settings entries
const existingSettings = await prisma.settings.findFirst();
if (!existingSettings) {
return NextResponse.json({ "notification_text": "" });
return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" });
}
// If settings entry exists, fetch it
@ -15,10 +15,10 @@ export async function POST(request: NextRequest) {
where: { id: existingSettings.id },
});
if (!settings) {
return NextResponse.json({ "notification_text": "" });
return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" });
}
// Return the settings entry
return NextResponse.json({ "notification_text": settings.notification_text });
return NextResponse.json({ "notification_text_application": settings.notification_text_application, "notification_text_server": settings.notification_text_server });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}

View File

@ -2,13 +2,14 @@ import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
interface AddRequest {
text: string;
text_application: string;
text_server: string;
}
export async function POST(request: NextRequest) {
try {
const body: AddRequest = await request.json();
const { text } = body;
const { text_application, text_server } = body;
// Check if there is already a settings entry
const existingSettings = await prisma.settings.findFirst();
@ -16,14 +17,15 @@ export async function POST(request: NextRequest) {
// Update the existing settings entry
const updatedSettings = await prisma.settings.update({
where: { id: existingSettings.id },
data: { notification_text: text },
data: { notification_text_application: text_application, notification_text_server: text_server },
});
return NextResponse.json({ message: "Success", updatedSettings });
}
// If no settings entry exists, create a new one
const settings = await prisma.settings.create({
data: {
notification_text: text,
notification_text_application: text_application,
notification_text_server: text_server,
}
});

View File

@ -72,83 +72,94 @@ export default function Dashboard() {
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-lg transition-all hover:shadow-xl hover:border-t-rose-600">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<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>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="grid grid-cols-2 gap-4">
{/* 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>
<CardFooter className="border-t bg-muted/10 p-4">
<Button
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>
</CardFooter>
</Card>
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<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="py-3 pb-1">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Applications</CardTitle>
<Layers className="h-6 w-6 text-amber-500" />
<div>
<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>
<CardDescription>Manage your deployed applications</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
<CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="grid grid-cols-2 gap-4">
{/* 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>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/applications">View all applications</Link>
<CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button
variant="outline"
size="default"
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
asChild
>
<Link href="/dashboard/servers" className="flex items-center justify-between">
<span>Manage Servers</span>
</Link>
</Button>
</CardFooter>
</Card>
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-lg transition-all hover:shadow-xl hover:border-t-amber-600">
<CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Uptime</CardTitle>
<Activity className="h-6 w-6 text-emerald-500" />
<div>
<CardTitle className="text-2xl font-semibold">Applications</CardTitle>
<CardDescription className="mt-1">Manage your deployed applications</CardDescription>
</div>
<Layers className="h-8 w-8 text-amber-500 p-1.5 rounded-lg" />
</div>
<CardDescription>Monitor your service availability</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="text-4xl font-bold">{applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
</CardContent>
<CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button
variant="outline"
size="default"
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
asChild
>
<Link href="/dashboard/applications" className="flex items-center justify-between">
<span>View all applications</span>
</Link>
</Button>
</CardFooter>
</Card>
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-lg transition-all hover:shadow-xl hover:border-t-emerald-600">
<CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-2xl font-semibold">Uptime</CardTitle>
<CardDescription className="mt-1">Monitor your service availability</CardDescription>
</div>
<Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" />
</div>
</CardHeader>
<CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="flex flex-col">
<div className="text-4xl font-bold flex items-center justify-between">
<span>
@ -169,28 +180,44 @@ export default function Dashboard() {
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
</div>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/uptime">View uptime metrics</Link>
<CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button
variant="outline"
size="default"
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
asChild
>
<Link href="/dashboard/uptime" className="flex items-center justify-between">
<span>View uptime metrics</span>
</Link>
</Button>
</CardFooter>
</Card>
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-lg transition-all hover:shadow-xl hover:border-t-sky-600">
<CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-medium">Network</CardTitle>
<Network className="h-6 w-6 text-sky-500" />
<div>
<CardTitle className="text-2xl font-semibold">Network</CardTitle>
<CardDescription className="mt-1">Manage network configuration</CardDescription>
</div>
<Network className="h-8 w-8 text-sky-500 p-1.5 rounded-lg" />
</div>
<CardDescription>Manage network configuration</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
</CardContent>
<CardFooter className="border-t bg-muted/20 p-4">
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
<Link href="/dashboard/network">View network details</Link>
<CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button
variant="outline"
size="default"
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
asChild
>
<Link href="/dashboard/network" className="flex items-center justify-between">
<span>View network details</span>
</Link>
</Button>
</CardFooter>
</Card>

View File

@ -73,6 +73,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { StatusIndicator } from "@/components/status-indicator";
interface Application {
id: number;
@ -115,21 +116,19 @@ export default function Dashboard() {
const [currentPage, setCurrentPage] = useState<number>(1);
const [maxPage, setMaxPage] = useState<number>(1);
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
const [applications, setApplications] = useState<Application[]>([]);
const [servers, setServers] = useState<Server[]>([]);
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [searchTerm, setSearchTerm] = useState<string>("");
const [isSearching, setIsSearching] = useState<boolean>(false);
useEffect(() => {
const savedLayout = Cookies.get("layoutPreference-app");
const layout_bool = savedLayout === "grid";
setIsGridLayout(layout_bool);
setItemsPerPage(layout_bool ? 15 : 5);
}, []);
const savedLayout = Cookies.get("layoutPreference-app");
const initialIsGridLayout = savedLayout === "grid";
const initialItemsPerPage = initialIsGridLayout ? 15 : 5;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const toggleLayout = () => {
const newLayout = !isGridLayout;
@ -440,20 +439,9 @@ export default function Dashboard() {
>
<CardHeader>
<div className="absolute top-2 right-2">
<div
className={`w-4 h-4 rounded-full flex items-center justify-center ${
app.online ? "bg-green-700" : "bg-red-700"
}`}
title={app.online ? "Online" : "Offline"}
>
<div
className={`w-2 h-2 rounded-full ${
app.online ? "bg-green-500" : "bg-red-500"
}`}
/>
</div>
<StatusIndicator isOnline={app.online} />
</div>
<div className="flex items-center justify-between w-full">
<div className="flex items-center justify-between w-full mt-4 mb-4">
<div className="flex items-center">
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
{app.icon ? (

View File

@ -24,7 +24,8 @@ import {
MicroscopeIcon as Microchip,
MemoryStick,
HardDrive,
Server,
LucideServer,
Copy,
} from "lucide-react"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
@ -57,7 +58,7 @@ 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"
import { StatusIndicator } from "@/components/status-indicator"
interface Server {
id: number
name: string
@ -73,6 +74,12 @@ interface Server {
disk?: string
hostedVMs?: Server[]
isVM?: boolean
monitoring?: boolean
monitoringURL?: string
online?: boolean
cpuUsage?: number
ramUsage?: number
diskUsage?: number
}
interface GetServersResponse {
@ -80,6 +87,14 @@ interface GetServersResponse {
maxPage: number
}
interface MonitoringData {
id: number
online: boolean
cpuUsage: number
ramUsage: number
diskUsage: number
}
export default function Dashboard() {
const [host, setHost] = useState<boolean>(false)
const [hostServer, setHostServer] = useState<number>(0)
@ -92,12 +107,16 @@ export default function Dashboard() {
const [gpu, setGpu] = useState<string>("")
const [ram, setRam] = useState<string>("")
const [disk, setDisk] = useState<string>("")
const [monitoring, setMonitoring] = useState<boolean>(false)
const [monitoringURL, setMonitoringURL] = useState<string>("")
const [online, setOnline] = useState<boolean>(false)
const [cpuUsage, setCpuUsage] = useState<number>(0)
const [ramUsage, setRamUsage] = useState<number>(0)
const [diskUsage, setDiskUsage] = useState<number>(0)
const [currentPage, setCurrentPage] = useState<number>(1)
const [maxPage, setMaxPage] = useState<number>(1)
const [itemsPerPage, setItemsPerPage] = useState<number>(4)
const [servers, setServers] = useState<Server[]>([])
const [isGridLayout, setIsGridLayout] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(true)
const [editId, setEditId] = useState<number | null>(null)
@ -112,6 +131,8 @@ export default function Dashboard() {
const [editGpu, setEditGpu] = useState<string>("")
const [editRam, setEditRam] = useState<string>("")
const [editDisk, setEditDisk] = useState<string>("")
const [editMonitoring, setEditMonitoring] = useState<boolean>(false)
const [editMonitoringURL, setEditMonitoringURL] = useState<string>("")
const [searchTerm, setSearchTerm] = useState<string>("")
const [isSearching, setIsSearching] = useState<boolean>(false)
@ -119,23 +140,25 @@ export default function Dashboard() {
const [hostServers, setHostServers] = useState<Server[]>([])
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 [monitoringInterval, setMonitoringInterval] = useState<NodeJS.Timeout | null>(null);
const savedLayout = Cookies.get("layoutPreference-servers");
const initialIsGridLayout = savedLayout === "grid";
const initialItemsPerPage = initialIsGridLayout ? 6 : 4;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
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); // Update itemsPerPage based on new layout
};
const add = async () => {
try {
@ -151,6 +174,8 @@ export default function Dashboard() {
gpu,
ram,
disk,
monitoring,
monitoringURL,
})
setIsAddDialogOpen(false)
setHost(false)
@ -164,6 +189,8 @@ export default function Dashboard() {
setGpu("")
setRam("")
setDisk("")
setMonitoring(false)
setMonitoringURL("")
getServers()
} catch (error: any) {
console.log(error.response.data)
@ -223,6 +250,8 @@ export default function Dashboard() {
setEditGpu(server.gpu || "")
setEditRam(server.ram || "")
setEditDisk(server.disk || "")
setEditMonitoring(server.monitoring || false)
setEditMonitoringURL(server.monitoringURL || "")
}
const edit = async () => {
@ -242,6 +271,8 @@ export default function Dashboard() {
gpu: editGpu,
ram: editRam,
disk: editDisk,
monitoring: editMonitoring,
monitoringURL: editMonitoringURL,
})
getServers()
setEditId(null)
@ -321,6 +352,78 @@ export default function Dashboard() {
// Flatten icons for search
const allIcons = Object.values(iconCategories).flat()
const copyServerDetails = (sourceServer: Server) => {
// First clear all fields
setName("")
setIcon("")
setOs("")
setIp("")
setUrl("")
setCpu("")
setGpu("")
setRam("")
setDisk("")
setMonitoring(false)
setMonitoringURL("")
setHost(false)
setHostServer(0)
// Then copy the new server details
setTimeout(() => {
setName(sourceServer.name + " (Copy)")
setIcon(sourceServer.icon || "")
setOs(sourceServer.os || "")
setIp(sourceServer.ip || "")
setUrl(sourceServer.url || "")
setCpu(sourceServer.cpu || "")
setGpu(sourceServer.gpu || "")
setRam(sourceServer.ram || "")
setDisk(sourceServer.disk || "")
setMonitoring(sourceServer.monitoring || false)
setMonitoringURL(sourceServer.monitoringURL || "")
setHost(sourceServer.host)
setHostServer(sourceServer.hostServer || 0)
}, 0)
}
const updateMonitoringData = async () => {
try {
const response = await axios.get<MonitoringData[]>("/api/servers/monitoring");
const monitoringData = response.data;
setServers(prevServers =>
prevServers.map(server => {
const serverMonitoring = monitoringData.find(m => m.id === server.id);
if (serverMonitoring) {
return {
...server,
online: serverMonitoring.online,
cpuUsage: serverMonitoring.cpuUsage,
ramUsage: serverMonitoring.ramUsage,
diskUsage: serverMonitoring.diskUsage
};
}
return server;
})
);
} catch (error) {
console.error("Error updating monitoring data:", error);
}
};
// Set up monitoring interval
useEffect(() => {
updateMonitoringData();
const interval = setInterval(updateMonitoringData, 5000);
setMonitoringInterval(interval);
return () => {
if (monitoringInterval) {
clearInterval(monitoringInterval);
}
};
}, []);
return (
<SidebarProvider>
<AppSidebar />
@ -368,13 +471,67 @@ export default function Dashboard() {
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Add an server</AlertDialogTitle>
<AlertDialogTitle className="flex justify-between items-center">
<span>Add a server</span>
<Select
onValueChange={(value) => {
if (!value) return;
const sourceServer = servers.find(s => s.id === parseInt(value));
if (!sourceServer) return;
// Clear all fields first
setName("");
setIcon("");
setOs("");
setIp("");
setUrl("");
setCpu("");
setGpu("");
setRam("");
setDisk("");
setMonitoring(false);
setMonitoringURL("");
setHost(false);
setHostServer(0);
// Copy new server details
setName(sourceServer.name + " (Copy)");
setIcon(sourceServer.icon || "");
setOs(sourceServer.os || "");
setIp(sourceServer.ip || "");
setUrl(sourceServer.url || "");
setCpu(sourceServer.cpu || "");
setGpu(sourceServer.gpu || "");
setRam(sourceServer.ram || "");
setDisk(sourceServer.disk || "");
setMonitoring(sourceServer.monitoring || false);
setMonitoringURL(sourceServer.monitoringURL || "");
setHost(sourceServer.host);
setHostServer(sourceServer.hostServer || 0);
}}
>
<SelectTrigger className="w-[140px] h-8 text-xs">
<div className="flex items-center gap-1.5">
<Copy className="h-3 w-3 text-muted-foreground" />
<SelectValue placeholder="Copy server" />
</div>
</SelectTrigger>
<SelectContent>
{servers.map((server) => (
<SelectItem key={server.id} value={server.id.toString()} className="text-sm">
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</AlertDialogTitle>
<AlertDialogDescription>
<Tabs defaultValue="general" className="w-full">
<TabsList className="w-full">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="hardware">Hardware</TabsTrigger>
<TabsTrigger value="virtualization">Virtualization</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
@ -448,6 +605,7 @@ export default function Dashboard() {
id="name"
type="text"
placeholder="e.g. Server1"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
@ -455,7 +613,7 @@ export default function Dashboard() {
<Label htmlFor="description">
Operating System <span className="text-stone-600">(optional)</span>
</Label>
<Select onValueChange={(value) => setOs(value)}>
<Select value={os} onValueChange={(value) => setOs(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select OS" />
</SelectTrigger>
@ -467,13 +625,14 @@ export default function Dashboard() {
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="icon">
<Label htmlFor="ip">
IP Adress <span className="text-stone-600">(optional)</span>
</Label>
<Input
id="icon"
id="ip"
type="text"
placeholder="e.g. 192.168.100.2"
value={ip}
onChange={(e) => setIp(e.target.value)}
/>
</div>
@ -495,6 +654,7 @@ export default function Dashboard() {
id="publicURL"
type="text"
placeholder="e.g. https://proxmox.server1.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
@ -503,46 +663,50 @@ export default function Dashboard() {
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
<Label htmlFor="cpu">
CPU <span className="text-stone-600">(optional)</span>
</Label>
<Input
id="name"
id="cpu"
type="text"
placeholder="e.g. AMD Ryzen™ 7 7800X3D"
value={cpu}
onChange={(e) => setCpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
<Label htmlFor="gpu">
GPU <span className="text-stone-600">(optional)</span>
</Label>
<Input
id="name"
id="gpu"
type="text"
placeholder="e.g. AMD Radeon™ Graphics"
value={gpu}
onChange={(e) => setGpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
<Label htmlFor="ram">
RAM <span className="text-stone-600">(optional)</span>
</Label>
<Input
id="name"
id="ram"
type="text"
placeholder="e.g. 64GB DDR5"
value={ram}
onChange={(e) => setRam(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
<Label htmlFor="disk">
Disk <span className="text-stone-600">(optional)</span>
</Label>
<Input
id="name"
id="disk"
type="text"
placeholder="e.g. 2TB SSD"
value={disk}
onChange={(e) => setDisk(e.target.value)}
/>
</div>
@ -563,12 +727,19 @@ export default function Dashboard() {
<Label>Host Server</Label>
<Select
value={hostServer?.toString()}
onValueChange={(value) => setHostServer(Number(value))}
onValueChange={(value) => {
const newHostServer = Number(value);
setHostServer(newHostServer);
if (newHostServer !== 0) {
setMonitoring(false);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">No host server</SelectItem>
{hostServers.map((server) => (
<SelectItem key={server.id} value={server.id.toString()}>
{server.name}
@ -580,6 +751,52 @@ export default function Dashboard() {
)}
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="monitoringCheckbox"
checked={monitoring}
onCheckedChange={(checked) => setMonitoring(checked === true)}
/>
<Label htmlFor="monitoringCheckbox">Enable monitoring</Label>
</div>
{monitoring && (
<>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="monitoringURL">Monitoring URL</Label>
<Input
id="monitoringURL"
type="text"
placeholder={`http://${ip}:61208`}
value={monitoringURL}
onChange={(e) => setMonitoringURL(e.target.value)}
/>
</div>
<div className="mt-4 p-4 border rounded-lg bg-muted">
<h4 className="text-sm font-semibold mb-2">Required Server Setup</h4>
<p className="text-sm text-muted-foreground mb-3">
To enable monitoring, you need to install Glances on your server. Here's an example Docker Compose configuration:
</p>
<pre className="bg-background p-4 rounded-md text-sm">
<code>{`services:
glances:
image: nicolargo/glances:latest
container_name: glances
restart: unless-stopped
ports:
- "61208:61208"
pid: "host"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- GLANCES_OPT=-w --disable-webui`}</code>
</pre>
</div>
</>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
@ -599,6 +816,7 @@ export default function Dashboard() {
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<br />
{!loading ? (
<div className={isGridLayout ? "grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-4" : "space-y-4"}>
@ -607,16 +825,23 @@ export default function Dashboard() {
.map((server) => (
<Card
key={server.id}
className={isGridLayout ? "h-full flex flex-col justify-between" : "w-full mb-4"}
className={`${isGridLayout ? "h-full flex flex-col justify-between" : "w-full mb-4"} hover:shadow-md transition-all duration-200 max-w-full relative`}
>
<CardHeader>
{server.monitoring && (
<div className="absolute top-2 right-2">
<StatusIndicator isOnline={server.online} />
</div>
)}
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<div className="ml-4">
<div className="flex items-center w-4/6">
<div className="ml-4 flex-grow">
<CardTitle className="text-2xl font-bold flex items-center gap-2">
<div className="flex items-center gap-2">
{server.icon && <DynamicIcon name={server.icon as any} color="white" size={24} />}
<span className=" font-bold">{server.icon && "・"} {server.name}</span>
<span className="font-bold">
{server.icon && "・"} {server.name}
</span>
</div>
{server.isVM && (
<span className="bg-blue-500 text-white text-xs px-2 py-1 rounded">VM</span>
@ -642,7 +867,7 @@ export default function Dashboard() {
{server.isVM && server.hostServer && (
<div className="flex items-center gap-2 text-foreground/80">
<Server className="h-4 w-4 text-muted-foreground" />
<LucideServer className="h-4 w-4 text-muted-foreground" />
<span>
<b>Host:</b> {getHostServerName(server.hostServer)}
</span>
@ -653,6 +878,10 @@ export default function Dashboard() {
<Separator />
</div>
<div className="col-span-full mb-2">
<h4 className="text-sm font-semibold">Hardware Information</h4>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span>
@ -677,10 +906,72 @@ export default function Dashboard() {
<b>Disk:</b> {server.disk || "-"}
</span>
</div>
{server.monitoring && server.hostServer === 0 && (
<>
<div className="col-span-full pt-2 pb-2">
<Separator />
</div>
<div className="col-span-full">
<h4 className="text-sm font-semibold mb-3">Resource Usage</h4>
<div className={`${!isGridLayout ? "grid grid-cols-3 gap-4" : "space-y-3"}`}>
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">CPU</span>
</div>
<span className="text-xs font-medium">{server.cpuUsage || 0}%</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${server.cpuUsage && server.cpuUsage > 80 ? "bg-destructive" : server.cpuUsage && server.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${server.cpuUsage || 0}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">RAM</span>
</div>
<span className="text-xs font-medium">{server.ramUsage || 0}%</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${server.ramUsage && server.ramUsage > 80 ? "bg-destructive" : server.ramUsage && server.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${server.ramUsage || 0}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Disk</span>
</div>
<span className="text-xs font-medium">{server.diskUsage || 0}%</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${server.diskUsage && server.diskUsage > 80 ? "bg-destructive" : server.diskUsage && server.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${server.diskUsage || 0}%` }}
/>
</div>
</div>
</div>
</div>
</>
)}
</CardDescription>
</div>
</div>
<div className="flex flex-col items-end justify-start space-y-2 w-[120px]">
<div className="w-1/6" />
<div className="flex flex-col items-end justify-start space-y-2 w-1/6">
<div className="flex items-center justify-end gap-2 w-full">
<div className="flex flex-col items-end gap-2">
<div className="flex gap-2">
@ -690,7 +981,7 @@ export default function Dashboard() {
className="gap-2"
onClick={() => window.open(server.url, "_blank")}
>
<Link className="h-4 w-4" />
<Link className="h-4 w-4" />
</Button>
)}
<Button
@ -716,6 +1007,7 @@ export default function Dashboard() {
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="hardware">Hardware</TabsTrigger>
<TabsTrigger value="virtualization">Virtualization</TabsTrigger>
{(!editHostServer || editHostServer === 0) && <TabsTrigger value="monitoring">Monitoring</TabsTrigger>}
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
@ -845,7 +1137,6 @@ export default function Dashboard() {
</div>
</div>
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
@ -905,12 +1196,19 @@ export default function Dashboard() {
<Label>Host Server</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(value) => setEditHostServer(Number(value))}
onValueChange={(value) => {
const newHostServer = Number(value);
setEditHostServer(newHostServer);
if (newHostServer !== 0) {
setEditMonitoring(false);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">No host server</SelectItem>
{hostServers
.filter((server) => server.id !== editId)
.map((server) => (
@ -924,6 +1222,52 @@ export default function Dashboard() {
)}
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="editMonitoringCheckbox"
checked={editMonitoring}
onCheckedChange={(checked) => setEditMonitoring(checked === true)}
/>
<Label htmlFor="editMonitoringCheckbox">Enable monitoring</Label>
</div>
{editMonitoring && (
<>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editMonitoringURL">Monitoring URL</Label>
<Input
id="editMonitoringURL"
type="text"
placeholder={`http://${editIp}:61208`}
value={editMonitoringURL}
onChange={(e) => setEditMonitoringURL(e.target.value)}
/>
</div>
<div className="mt-4 p-4 border rounded-lg bg-muted">
<h4 className="text-sm font-semibold mb-2">Required Server Setup</h4>
<p className="text-sm text-muted-foreground mb-3">
To enable monitoring, you need to install Glances on your server. Here's an example Docker Compose configuration:
</p>
<pre className="bg-background p-4 rounded-md text-sm">
<code>{`services:
glances:
image: nicolargo/glances:latest
container_name: glances
restart: unless-stopped
ports:
- "61208:61208"
pid: "host"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- GLANCES_OPT=-w --disable-webui`}</code>
</pre>
</div>
</>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
@ -939,7 +1283,7 @@ export default function Dashboard() {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="h-9 flex items-center gap-2 px-3 w-full">
<Server className="h-4 w-4" />
<LucideServer className="h-4 w-4" />
<span>VMs</span>
</Button>
</AlertDialogTrigger>
@ -965,7 +1309,10 @@ export default function Dashboard() {
size={24}
/>
)}
<div className="text-base font-extrabold">{hostedVM.icon && "・ "}{hostedVM.name}</div>
<div className="text-base font-extrabold">
{hostedVM.icon && "・ "}
{hostedVM.name}
</div>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Button
@ -1227,23 +1574,25 @@ export default function Dashboard() {
<Label>Host Server</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(value) =>
setEditHostServer(Number(value))
}
onValueChange={(value) => {
const newHostServer = Number(value);
setEditHostServer(newHostServer);
if (newHostServer !== 0) {
setEditMonitoring(false);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">No host server</SelectItem>
{hostServers
.filter(
(server) => server.id !== editId,
)
.map((server) => (
<SelectItem
key={server.id}
value={server.id.toString()}
>
<SelectItem key={server.id} value={server.id.toString()}>
{server.name}
</SelectItem>
))}
@ -1284,6 +1633,10 @@ export default function Dashboard() {
</div>
</div>
<div className="col-span-full mb-2">
<h4 className="text-sm font-semibold">Hardware Information</h4>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span>
@ -1308,6 +1661,70 @@ export default function Dashboard() {
<b>Disk:</b> {hostedVM.disk || "-"}
</span>
</div>
{hostedVM.monitoring && (
<>
<div className="col-span-full pt-2 pb-2">
<Separator />
</div>
<div className="col-span-full grid grid-cols-3 gap-4">
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">CPU</span>
</div>
<span className="text-xs font-medium">
{hostedVM.cpuUsage || 0}%
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${hostedVM.cpuUsage && hostedVM.cpuUsage > 80 ? "bg-destructive" : hostedVM.cpuUsage && hostedVM.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${hostedVM.cpuUsage || 0}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">RAM</span>
</div>
<span className="text-xs font-medium">
{hostedVM.ramUsage || 0}%
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${hostedVM.ramUsage && hostedVM.ramUsage > 80 ? "bg-destructive" : hostedVM.ramUsage && hostedVM.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${hostedVM.ramUsage || 0}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Disk</span>
</div>
<span className="text-xs font-medium">
{hostedVM.diskUsage || 0}%
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
<div
className={`h-full ${hostedVM.diskUsage && hostedVM.diskUsage > 80 ? "bg-destructive" : hostedVM.diskUsage && hostedVM.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
style={{ width: `${hostedVM.diskUsage || 0}%` }}
/>
</div>
</div>
</div>
</>
)}
</div>
))}
</div>

View File

@ -39,7 +39,8 @@ interface NotificationsResponse {
notifications: any[]
}
interface NotificationResponse {
notification_text?: string
notification_text_application?: string
notification_text_server?: string
}
export default function Settings() {
@ -73,10 +74,14 @@ export default function Settings() {
const [gotifyToken, setGotifyToken] = useState<string>("")
const [ntfyUrl, setNtfyUrl] = useState<string>("")
const [ntfyToken, setNtfyToken] = useState<string>("")
const [pushoverUrl, setPushoverUrl] = useState<string>("")
const [pushoverToken, setPushoverToken] = useState<string>("")
const [pushoverUser, setPushoverUser] = useState<string>("")
const [notifications, setNotifications] = useState<any[]>([])
const [notificationText, setNotificationText] = useState<string>("")
const [notificationTextApplication, setNotificationTextApplication] = useState<string>("")
const [notificationTextServer, setNotificationTextServer] = useState<string>("")
const changeEmail = async () => {
setEmailErrorVisible(false)
@ -176,6 +181,9 @@ export default function Settings() {
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken,
pushoverUser: pushoverUser,
})
getNotifications()
} catch (error: any) {
@ -215,10 +223,15 @@ export default function Settings() {
try {
const response = await axios.post<NotificationResponse>("/api/settings/get_notification_text", {})
if (response.status === 200) {
if (response.data.notification_text) {
setNotificationText(response.data.notification_text)
if (response.data.notification_text_application) {
setNotificationTextApplication(response.data.notification_text_application)
} else {
setNotificationText("The application !name (!url) is now !status.")
setNotificationTextApplication("The application !name (!url) is now !status.")
}
if (response.data.notification_text_server) {
setNotificationTextServer(response.data.notification_text_server)
} else {
setNotificationTextServer("The server !name is now !status.")
}
}
} catch (error: any) {
@ -229,7 +242,8 @@ export default function Settings() {
const editNotificationText = async () => {
try {
const response = await axios.post("/api/settings/notification_text", {
text: notificationText,
text_application: notificationTextApplication,
text_server: notificationTextServer,
})
} catch (error: any) {
alert(error.response.data.error)
@ -433,6 +447,7 @@ export default function Settings() {
<SelectItem value="discord">Discord</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem>
</SelectContent>
{notificationType === "smtp" && (
@ -593,6 +608,40 @@ export default function Settings() {
</div>
</div>
)}
{notificationType === "pushover" && (
<div className="mt-4 flex flex-col gap-2">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="pushoverUrl">Pushover URL</Label>
<Input
type="text"
id="pushoverUrl"
placeholder="e.g. https://api.pushover.net/1/messages.json"
onChange={(e) => setPushoverUrl(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="pushoverToken">Pushover Token</Label>
<Input
type="text"
id="pushoverToken"
placeholder="e.g. 1234567890"
onChange={(e) => setPushoverToken(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="pushoverUser">Pushover User</Label>
<Input
type="text"
id="pushoverUser"
placeholder="e.g. 1234567890"
onChange={(e) => setPushoverUser(e.target.value)}
/>
</div>
</div>
)}
</Select>
</AlertDialogDescription>
<AlertDialogFooter>
@ -613,12 +662,22 @@ export default function Settings() {
<AlertDialogDescription>
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="text">Notification Text</Label>
<Label htmlFor="text_application">Notification Text for Applications</Label>
<Textarea
id="text"
id="text_application"
placeholder="Type here..."
value={notificationText}
onChange={(e) => setNotificationText(e.target.value)}
value={notificationTextApplication}
onChange={(e) => setNotificationTextApplication(e.target.value)}
rows={4}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="text_server">Notification Text for Servers</Label>
<Textarea
id="text_server"
placeholder="Type here..."
value={notificationTextServer}
onChange={(e) => setNotificationTextServer(e.target.value)}
rows={4}
/>
</div>
@ -627,13 +686,19 @@ export default function Settings() {
You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2">
<li>
<strong>!name</strong> - Application name
<b>Server related:</b>
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
<li>!name - The name of the server</li>
<li>!status - The current status of the server (online/offline)</li>
</ul>
</li>
<li>
<strong>!url</strong> - Application URL
</li>
<li>
<strong>!status</strong> - Application status (online/offline)
<b>Application related:</b>
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
<li>!name - The name of the application</li>
<li>!url - The URL where the application is hosted</li>
<li>!status - The current status of the application (online/offline)</li>
</ul>
</li>
</ul>
</div>
@ -681,6 +746,11 @@ export default function Settings() {
<Bell className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "pushover" && (
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
)}
<div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3>
<p className="text-xs text-muted-foreground">
@ -689,6 +759,7 @@ export default function Settings() {
{notification.type === "discord" && "Discord webhook alerts"}
{notification.type === "gotify" && "Gotify notifications"}
{notification.type === "ntfy" && "Ntfy notifications"}
{notification.type === "pushover" && "Pushover notifications"}
</p>
</div>
</div>

View File

@ -0,0 +1,38 @@
"use client"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
interface StatusIndicatorProps {
isOnline?: boolean
className?: string
showLabel?: boolean
pulseAnimation?: boolean
}
export function StatusIndicator({
isOnline = false,
className,
showLabel = true,
pulseAnimation = true,
}: StatusIndicatorProps) {
return (
<Badge
variant="outline"
className={cn(
"flex items-center gap-2 px-2 py-1 border-transparent transition-colors duration-300",
isOnline
? "bg-green-100 dark:bg-green-950/30 text-green-800 dark:text-green-300"
: "bg-red-100 dark:bg-red-950/30 text-red-800 dark:text-red-300",
className,
)}
>
<span className={cn("relative flex h-2.5 w-2.5 rounded-full", isOnline ? "bg-green-500" : "bg-red-500")}>
{isOnline && pulseAnimation && (
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
)}
</span>
{showLabel && <span className="text-xs font-medium">{isOnline ? "Online" : "Offline"}</span>}
</Badge>
)
}

View File

@ -6,9 +6,6 @@ services:
environment:
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
- db
- agent
agent:
image: haedlessdev/corecontrol-agent:latest

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1,65 @@
import { defineConfig } from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "CoreControl",
description: "Dashboard to manage your entire server infrastructure",
lastUpdated: true,
cleanUrls: true,
metaChunk: true,
head: [
['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }],
],
themeConfig: {
logo: '/logo.png',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Installation', link: '/installation' }
],
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025-present CoreControl'
},
search: {
provider: 'local',
},
sidebar: [
{
text: 'Deploy',
items: [
{ text: 'Installation', link: '/installation' },
]
},
{
text: 'General',
items: [
{ text: 'Dashboard', link: '/general/dashboard' },
{ text: 'Servers', link: '/general/servers' },
{ text: 'Applications', link: '/general/applications' },
{ text: 'Uptime', link: '/general/uptime' },
{ text: 'Network', link: '/general/network' },
{ text: 'Settings', link: '/general/settings' },
]
},
{
text: 'Notifications',
items: [
{ text: 'Notifications', link: '/notifications/general' },
{ text: 'Email', link: '/notifications/email' },
{ text: 'Telegram', link: '/notifications/telegram' },
{ text: 'Discord', link: '/notifications/discord' },
{ text: 'Gotify', link: '/notifications/gotify' },
{ text: 'Ntfy', link: '/notifications/ntfy' },
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/crocofied/corecontrol' }
]
}
})

View File

@ -1,27 +0,0 @@
---
icon: gear
layout:
title:
visible: true
description:
visible: true
tableOfContents:
visible: true
outline:
visible: false
pagination:
visible: true
---
# CoreControl
<figure><img src=".gitbook/assets/image.png" alt=""><figcaption></figcaption></figure>
CoreControl is the only dashboard you'll ever need to manage your entire server infrastructure. Keep all your server data organized in one central place, easily add your self-hosted applications with quick access links, and monitor their availability in real-time with built-in uptime tracking. Designed for simplicity and control, it gives you a clear overview of your entire self-hosted setup at a glance.
## Features
* Dashboard: A clear screen with all the important information about your servers (WIP)
* Servers: This allows you to add all your servers (including Hardware Information), with Quicklinks to their Management Panels
* Applications: Add all your self-hosted services to a clear list and track their up and down time
* Networks: Generate visually stunning network flowcharts with ease.

View File

@ -1,4 +0,0 @@
# Table of contents
* [CoreControl](README.md)
* [Installation](installation.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,27 @@
# Applications
All your self-hosted applications are displayed here.
## Add an application
To add a new application to CoreControl, follow these steps:
1. Click the "Add Application" button in the top right corner of the server menu:
![Application Add Button](../assets/screenshots/applications_add_button.png)
2. Fill out the server details across the following information:
- **Name**: Enter the name of the application
- **Server**: Select the server on which the application is running
- **Description**: Enter a short (or long) description of the server
- **Icon URL**: Add the url pointing to the logo of the application. With the flash button the logo will be automatically selected.
- **Public URL**: Enter the public URL of your application. This will be used to track the uptime.
- **Local URL**: Enter the local URL of your application, i.e. the URL via which the application is only accessible in the local network
After filling out the required information, click "Add" to add the application to CoreControl.
## Application Display
Your applications are displayed in a list or grid (depending on the display settings) - each application in its own card
![Application card](../assets/screenshots/applications_display.png)

31
docs/general/Dashboard.md Normal file
View File

@ -0,0 +1,31 @@
# Dashboard
The dashboard is the most important place to get a quick overview of your infrastructure.
## Cards Overview
The dashboard is divided into 4 cards that provide different aspects of your infrastructure monitoring:
### Servers Card
![Servers Card](../assets/screenshots/dashboard_card_servers.png)
The Servers card displays information about all your connected servers, including:
- Number of Physical Servers
- Number of Virtual Servers
### Applications Card
![Applications Card](../assets/screenshots/dashboard_card_applications.png)
The Applications card shows you:
- Number of running applications across your infrastructure
### Uptime Card
![Uptime Card](../assets/screenshots/dashboard_card_uptime.png)
The Uptime card provides:
- Number of online applications
### Network Card
![Network Card](../assets/screenshots/dashboard_card_network.png)
The Network card displays:
- Sum of servers and applications

3
docs/general/Network.md Normal file
View File

@ -0,0 +1,3 @@
# Network
A network flowchart is automatically generated on this page, which shows the connections of your infrastructure. The main servers are displayed based on the main node “My Infrastrucutre”. Below this are the applications running directly on this server and next to it the VMs running on the server, if it is a host server. To the right of the VMs, all applications running on the respective VM are listed.

80
docs/general/Servers.md Normal file
View File

@ -0,0 +1,80 @@
# Servers
In the server menu you can see all your servers and add more if required
## Add a Server
To add a new server to CoreControl, follow these steps:
1. Click the "Add Server" button in the top right corner of the server menu:
![Servers Add Button](../assets/screenshots/servers_add_button.png)
2. Fill out the server details across the following tabs:
### General Tab
Configure the basic server information:
- **Icon**: Choose a custom icon for your server
- **Name**: Enter a descriptive name for the server
- **Operating System**: Select the server's operating system
- **IP Address**: Enter the server's IP address
- **Management URL**: Add the URL used to manage the server (optional)
### Hardware Tab
Specify the server's hardware specifications:
- **CPU**: Enter CPU model and specifications
- **GPU**: Add graphics card details if applicable
- **RAM**: Specify the amount of RAM
- **Disk**: Enter storage capacity and configuration
### Virtualization Tab
Configure virtualization settings:
- **Host Server Settings**:
- Enable "Host Server" if this server will host virtual machines
- Perfect for hypervisors like Proxmox, VMware, or similar
- **VM Settings**:
- Select a host server if this server is a virtual machine
- This creates a logical connection between the VM and its host
### Monitoring Tab
Set up server monitoring options (see "Monitoring" section for detailed information)
After filling out the required information, click "Add" to add the server to CoreControl.
## Monitoring
If you want to monitor the hardware usage and status of your servers, you will have to enable monitoring in the monitoring tab.
After you have done this you need to install [Glances](https://github.com/nicolargo/glances) on the server. To help you with this, we have created a sample compose that you can simply copy. For detailed customizations, please refer to the [Glances docs](https://glances.readthedocs.io/en/latest/).
```yaml
services:
glances:
image: nicolargo/glances:latest
container_name: glances
restart: unless-stopped
ports:
- "61208:61208"
pid: "host"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- GLANCES_OPT=-w --disable-webui
```
::: warning
Please also make sure that CoreControl can reach the specified API URL of Glances. In addition, the Glances API URL should be specified in the format `http://<IP_OF_SERVER>:61208`.
:::
## Server Display
Your servers are displayed in a list or grid (depending on the display settings) - each server in its own card
![Server Card](../assets/screenshots/servers_display.png)
There are also three action buttons at the end of each card.
- Link Button - With this you can open the specified management URL of the server with one click
- Delete Button - Direct deletion of the server
- Edit Button - Customize the server with the same menu as when creating the server
## VMs
If a host server contains VMs, you can display them using the “VMs” button
![VMs Button](../assets/screenshots/servers_vms_button.png)
The associated VMs are then displayed in a clearly arranged list.
![VM List](../assets/screenshots/servers_vms_list.png)

11
docs/general/Settings.md Normal file
View File

@ -0,0 +1,11 @@
# Settings
Here you can manage the complete settings of CoreControl.
## User Settings
![User Settings](../assets/screenshots/settings_user.png)
## Theme Settings
![Theme Settings](../assets/screenshots/settings_theme.png)
## Notification Settings
![Notification Settings](../assets/screenshots/settings_notifications.png)

7
docs/general/Uptime.md Normal file
View File

@ -0,0 +1,7 @@
# Uptime
The uptime of all your Applications is shown here in a clear list.
![Uptime](../assets/screenshots/uptime.png)
With the Select menu you can also filter the time span (30min, 7 days and 30 days)

44
docs/index.md Normal file
View File

@ -0,0 +1,44 @@
---
layout: home
hero:
name: "CoreControl"
text: "Manage your server infrastructure"
actions:
- theme: brand
text: Install
link: /installation
- theme: alt
text: GitHub
link: https://github.com/crocofied/corecontrol
image:
src: /logo.png
alt: Logo
features:
- icon: 🚀
title: Easy Deployment
details: Deploy and manage your servers with just a few clicks - thanks to docker
- icon: 🔒
title: Secure Management
details: Secure connections with the panel and a more secure authentication system
- icon: 📊
title: Real-time Monitoring
details: Monitor server performance, resource usage and uptime in real-time
- icon: 🎮
title: Easy to Manage
details: Simple and intuitive management interface for all your needs
- icon: 🔔
title: Notifications
details: Stay informed withalerts and notifications about your servers & applications status
- icon: ✨
title: Clean UI
details: Modern and user-friendly interface designed for the best user experience
---
<style>
:root {
--vp-home-hero-image-background-image: linear-gradient(rgba(255,255,255,0.25), rgba(255,255,255,0.25));
--vp-home-hero-image-filter: blur(100px);
}
</style>

View File

@ -1,12 +1,16 @@
---
icon: down
---
# Installation
To install the application using Docker Compose, first, ensure that Docker and Docker Compose are installed on your system.&#x20;
The easiest way to install CoreControl is using Docker Compose. Follow these steps:
You can then simply install and start the following Docker compose. Remember that you have to generate a JWT\_SECRET beforehand.
## Docker Compose Installation
::: danger
CoreControl is at an early stage of development and is subject to change. It is not recommended for use in a production environment at this time.
:::
1. Make sure [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your system.
2. Create a file named `docker-compose.yml` with the following content:
```yaml
services:
@ -17,14 +21,14 @@ services:
environment:
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
- db
- agent
agent:
image: haedlessdev/corecontrol-agent:latest
environment:
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
@ -35,22 +39,36 @@ services:
POSTGRES_DB: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 2s
retries: 10
volumes:
postgres_data:
```
Now start the application:
3. Generate a custom JWT_SECRET with e.g. [jwtsecret.com/generate](https://jwtsecret.com/generate)
3. Start CoreControl with the following command:
```sh
```bash
docker-compose up -d
# OR
docker compose up -d
```
5. The application is now available at `http://localhost:3000`.
## Authentication
**The default login is:**
CoreControl comes with a default administrator account:
E-Mail: [admin@example.com](mailto:admin@example.com)\
Password: admin
- **Email**: admin@example.com
- **Password**: admin
_Be sure to set your own password and customize the e-mail, otherwise this poses a security risk!_
::: warning
For security reasons, it is strongly recommended to change the default credentials immediately after your first login.
:::
You can change the administrator password in the settings after logging in.

View File

@ -0,0 +1,3 @@
# Discord
![Discord](../assets/screenshots/notifications_discord.png)

View File

@ -0,0 +1,3 @@
# Email
![Set up](../assets/screenshots/notifications_smtp.png)

View File

@ -0,0 +1,6 @@
# Notifications
You can set the notifications for CoreControl in the settings. These notifications include when an application goes online or offline and when a server goes online or offline.
![Notification Settings](../assets/screenshots/settings_notifications.png)
You can also customize direct notification texts and improve them with placeholders

View File

@ -0,0 +1,3 @@
# Gotify
![Set up](../assets/screenshots/notifications_gotify.png)

View File

@ -0,0 +1,3 @@
# Ntfy
![Set up](../assets/screenshots/notifications_ntfy.png)

View File

@ -0,0 +1,3 @@
# Telegram
![Telegram](../assets/screenshots/notifications_telegram.png)

2422
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
docs/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "docs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"vitepress": "^1.6.3"
}
}

5
docs/postcss.config.cjs Normal file
View File

@ -0,0 +1,5 @@
// docs/postcss.config.cjs
module.exports = {
plugins: []
}

BIN
docs/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

2792
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "corecontrol",
"version": "0.0.7",
"version": "0.0.8",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@ -40,6 +40,7 @@
"lucide-react": "^0.487.0",
"next": "15.3.0",
"next-themes": "^0.4.6",
"postcss-loader": "^8.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.2.0",
@ -50,8 +51,10 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"prisma": "^6.6.0",
"tailwindcss": "^4",
"tailwindcss": "^4.1.4",
"tsx": "^4.19.3",
"typescript": "^5"
}

View File

@ -2,4 +2,4 @@ const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
export default config;

View File

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "server" ADD COLUMN "cpuUsage" TEXT,
ADD COLUMN "diskUsage" TEXT,
ADD COLUMN "monitoring" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "monitoringURL" TEXT,
ADD COLUMN "online" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "ramUsage" TEXT;

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `notification_text` on the `settings` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "settings" DROP COLUMN "notification_text",
ADD COLUMN "notification_text_application" TEXT,
ADD COLUMN "notification_text_server" TEXT;

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "server_history" (
"id" SERIAL NOT NULL,
"serverId" INTEGER NOT NULL DEFAULT 1,
"online" BOOLEAN NOT NULL DEFAULT true,
"cpuUsage" TEXT,
"ramUsage" TEXT,
"diskUsage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "server_history_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "notification" ADD COLUMN "pushoverToken" TEXT,
ADD COLUMN "pushoverUser" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "notification" ADD COLUMN "pushoverUrl" TEXT;

View File

@ -32,6 +32,16 @@ model uptime_history {
createdAt DateTime @default(now())
}
model server_history {
id Int @id @default(autoincrement())
serverId Int @default(1)
online Boolean @default(true)
cpuUsage String?
ramUsage String?
diskUsage String?
createdAt DateTime @default(now())
}
model server {
id Int @id @default(autoincrement())
host Boolean @default(false)
@ -45,12 +55,19 @@ model server {
gpu String?
ram String?
disk String?
monitoring Boolean @default(false)
monitoringURL String?
cpuUsage String?
ramUsage String?
diskUsage String?
online Boolean @default(true)
}
model settings {
id Int @id @default(autoincrement())
uptime_checks Boolean @default(true)
notification_text String?
notification_text_application String?
notification_text_server String?
}
model user {
@ -77,4 +94,7 @@ model notification {
gotifyToken String?
ntfyUrl String?
ntfyToken String?
pushoverUrl String?
pushoverToken String?
pushoverUser String?
}