72 Commits

Author SHA1 Message Date
headlessdev
828b1e4fbe Docs Hotfix 2025-04-21 16:06:35 +02:00
headlessdev
686088e08c docs build 2025-04-21 16:02:55 +02:00
headlessdev
f164f9dea2 Update README.md 2025-04-21 15:53:04 +02:00
headlessdev
10acf3c738 v0.0.8->main
V0.0.8
2025-04-21 15:51:17 +02:00
headlessdev
9ee8d28fe8 Update .dockerignore to include 'docs/' directory 2025-04-21 15:43:21 +02:00
headlessdev
c52581e4e3 Update README with new dashboard and applications page images, and mark Simple Server Monitoring History as a task 2025-04-21 15:42:54 +02:00
headlessdev
5b411b9f3d Fix spacing in Pushover add notifcation 2025-04-21 15:39:10 +02:00
headlessdev
1063072757 Pushover Placeholders 2025-04-21 15:35:38 +02:00
headlessdev
93841120aa Add Pushover notification to go 2025-04-21 15:34:56 +02:00
headlessdev
27a63a9025 Add Pushover Configuration to frontend 2025-04-21 15:31:38 +02:00
headlessdev
d51908b48d Add pushover to notification add api 2025-04-21 15:28:36 +02:00
headlessdev
e0c159cb71 pushover settings in notifcations model 2025-04-21 15:27:40 +02:00
headlessdev
ce9bdadb69 Delete server_history of a server on server deletion 2025-04-21 15:20:10 +02:00
headlessdev
082e36a34c Updated Attribution 2025-04-21 15:16:56 +02:00
headlessdev
861ed28e45 Updated Docs 2025-04-21 15:14:40 +02:00
headlessdev
8dade95c75 agent notifications for server offline/online 2025-04-21 13:31:07 +02:00
headlessdev
1ae8b3e324 server_history model 2025-04-21 13:25:55 +02:00
headlessdev
21dd61c597 Update notification text handling in API and dashboard to support separate application and server notification texts. 2025-04-21 13:22:40 +02:00
headlessdev
49eeab4848 Refactor settings model in Prisma schema to rename notification_text field to notification_text_application and add notification_text_server field for improved clarity and organization. 2025-04-21 13:15:30 +02:00
headlessdev
f47e22fe27 Dashboard UI Fix 2025-04-21 13:11:37 +02:00
headlessdev
809bf19eb4 Grid/List View Pagination Fix 2025-04-21 12:56:32 +02:00
headlessdev
a1a5e5e299 Updated Status indicator 2025-04-21 12:40:55 +02:00
headlessdev
df2c788b0b periodic monitoring data updates from the server API. 2025-04-21 12:27:18 +02:00
headlessdev
693368b735 Updated docs 2025-04-21 12:16:53 +02:00
headlessdev
ded9149466 Copy Server Function 2025-04-21 00:13:27 +02:00
headlessdev
f40e588e7d sample Docker Compose configuration for Glances 2025-04-20 23:39:54 +02:00
headlessdev
fdbac4ebff Enhance Servers component by adding a "No host server" option in the server selection dropdown, updating state management to handle monitoring settings based on selected host server, and introducing a new "Hardware Information" section for better organization of server details. 2025-04-20 23:34:57 +02:00
headlessdev
456f40dab2 Updated Status indicator on Server card 2025-04-20 23:25:35 +02:00
headlessdev
ca81165a1c Enhance Servers component with detailed resource usage metrics display, including CPU, RAM, and Disk usage indicators, and update server icon to LucideServer. 2025-04-20 23:19:58 +02:00
headlessdev
f548367e86 Add server monitoring functionality, including server struct, metrics retrieval for CPU, memory, and disk usage, and status updates in the database. 2025-04-20 22:45:54 +02:00
headlessdev
0975bc5c5f Documentation Init 2025-04-20 19:54:58 +02:00
headlessdev
2eb0b4c8a0 Vitepress init 2025-04-20 18:44:27 +02:00
headlessdev
9036110a1c Remove gitbook 2025-04-20 18:37:38 +02:00
headlessdev
7f2ebf6129 Docker Compose depends_on fix 2025-04-20 17:16:10 +02:00
headlessdev
4a647ac19b Add monitoring fields to server add and edit routes, including monitoring status and URL in request handling. 2025-04-20 17:10:26 +02:00
headlessdev
d1549bf096 Add monitoring functionality to Servers component, including state management for monitoring settings and a new monitoring tab in the UI. 2025-04-20 17:09:33 +02:00
headlessdev
a3d1814343 Add monitoring fields and online status to server model in Prisma schema 2025-04-19 21:47:00 +02:00
headlessdev
6c8d4c6ec1 Bump version 2025-04-19 19:44:54 +02:00
headlessdev
63e8744e78 v0.0.7->main
v0.0.7
2025-04-19 16:34:14 +02:00
headlessdev
e39402ff70 small notification text fix 2025-04-19 16:23:53 +02:00
headlessdev
7d49baee6b Update server count query to filter by hostServer set to 0 and null to fix pagination 2025-04-19 16:20:59 +02:00
headlessdev
ceb10a2ffe Update server name display to conditionally include an icon indicator for better visual context. 2025-04-19 16:18:44 +02:00
headlessdev
7986737e0e Update Applications component layout by adjusting width of URL buttons and refining button labels for clarity. 2025-04-19 16:15:56 +02:00
headlessdev
f6debb1629 AAdjusted server name display to include an icon indicator and refined button alignment for management URL access. 2025-04-19 16:12:41 +02:00
headlessdev
8259563c33 Refactor Servers component to improve code readability and structure. Removed unused imports, standardized import statements, and updated state variable declarations for consistency. Enhanced icon selection functionality in the server editing interface. 2025-04-19 15:58:48 +02:00
headlessdev
3580f7f640 Add icon field to AddRequest and EditRequest interfaces, and update Dashboard component to support icon selection and display. Enhanced server creation and editing functionality with icon integration. 2025-04-19 15:46:01 +02:00
headlessdev
b655b7fe2d Add optional icon field to server model in Prisma schema 2025-04-19 15:17:20 +02:00
headlessdev
b42c1a45cc Refactor Servers component by removing unused imports and cleaning up code structure 2025-04-19 15:07:34 +02:00
headlessdev
86d48bc082 VMs are now shown in the search results 2025-04-19 14:39:02 +02:00
headlessdev
44817d6685 Adjust flowchart spacing constants for improved layout and readability 2025-04-19 14:26:42 +02:00
headlessdev
62c27118d6 Add support for Gotify and Ntfy notification types in the agent module
- Introduced new fields for Gotify and Ntfy URLs and tokens in the Notification struct.
- Updated the loadNotifications function to retrieve Gotify and Ntfy data from the database.
- Implemented sendGotify and sendNtfy functions to handle sending notifications via Gotify and Ntfy services.
- Enhanced the sendNotifications function to include logic for sending messages through Gotify and Ntfy.
2025-04-19 13:48:52 +02:00
headlessdev
016c9a2562 Remove color styling from server count display in Dashboard 2025-04-19 13:24:08 +02:00
headlessdev
b835ded157 Add notification type descriptions and icons for Gotify and Ntfy in Settings component 2025-04-19 13:21:39 +02:00
headlessdev
016d52fa1b Remove Bell icon from Add Notification Channel button in Settings component 2025-04-19 13:20:47 +02:00
headlessdev
2b8f7a95d2 Add Gotify and Ntfy configuration fields to Settings component for enhanced notification options 2025-04-19 13:20:04 +02:00
headlessdev
300547e59e Add Gotify and ntfy fields to AddRequest interface and update POST method to handle new notification types 2025-04-19 13:11:18 +02:00
headlessdev
93bffa29cc Add new fields for Gotify and ntfy integration in notification model 2025-04-19 13:07:09 +02:00
headlessdev
2a910c165e Add disabled state and warning message for host server checkbox in Dashboard 2025-04-19 00:29:45 +02:00
headlessdev
6412cbaf1c Filter out the currently edited server from the host server selection in the Dashboard component for improved user experience. 2025-04-19 00:28:22 +02:00
headlessdev
d9304001fe Fix ScrollArea width in Dashboard server card for better layout consistency 2025-04-19 00:20:38 +02:00
headlessdev
7d7897c3f6 Update server card title in Dashboard for clarity 2025-04-19 00:18:02 +02:00
headlessdev
1ae55da3f9 UI improvements server card 2025-04-19 00:17:41 +02:00
headlessdev
113bb3bfb4 Enhance Dashboard UI for server management by updating card layout, improving titles, and adding descriptions for physical and virtual servers. 2025-04-19 00:16:23 +02:00
headlessdev
83ea20545d Update server count handling in Dashboard component to separate physical servers and VMs 2025-04-19 00:13:12 +02:00
headlessdev
965f79f31a Refactor server count retrieval in POST request to differentiate between servers with and without VMs 2025-04-19 00:00:10 +02:00
headlessdev
f1c0cc9deb Network VM spacing fix 2025-04-18 23:57:10 +02:00
headlessdev
f2535cd2b9 Refactor hostServer assignment in PUT request to handle null values correctly 2025-04-18 23:48:29 +02:00
headlessdev
42e584a381 Updated Flowchart 2025-04-18 23:46:20 +02:00
headlessdev
0e1f9edaab improved ui for setting notifications card 2025-04-18 23:03:05 +02:00
headlessdev
c3fe3bc03d Update version to 0.0.7 in package.json 2025-04-18 22:53:02 +02:00
headlessdev
67097725d7 Remove deprecated @next/swc-win32-x64-msvc module from package-lock.json 2025-04-18 22:50:38 +02:00
headlessdev
61468a359d Update README.md 2025-04-18 17:12:49 +02:00
162 changed files with 8880 additions and 1325 deletions

View File

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

5
.gitignore vendored
View File

@@ -1,5 +1,10 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Vitepress
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/SYwrFw8/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,14 +61,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
@@ -78,6 +79,11 @@ 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:
@@ -97,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

@@ -1,9 +1,11 @@
package main
import (
"bytes"
"context"
"crypto/x509"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net"
@@ -26,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
@@ -40,6 +67,13 @@ type Notification struct {
TelegramChatID sql.NullString
TelegramToken sql.NullString
DiscordWebhook sql.NullString
GotifyUrl sql.NullString
GotifyToken sql.NullString
NtfyUrl sql.NullString
NtfyToken sql.NullString
PushoverUrl sql.NullString
PushoverToken sql.NullString
PushoverUser sql.NullString
}
var (
@@ -102,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)
}
}
@@ -134,7 +182,7 @@ func isIPAddress(host string) bool {
func loadNotifications(db *sql.DB) ([]Notification, error) {
rows, err := db.Query(
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
"telegramChatId", "telegramToken", "discordWebhook"
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken"
FROM notification
WHERE enabled = true`,
)
@@ -149,7 +197,7 @@ func loadNotifications(db *sql.DB) ([]Notification, error) {
if err := rows.Scan(
&n.ID, &n.Enabled, &n.Type,
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo,
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook,
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
); err != nil {
fmt.Printf("Error scanning notification: %v\n", err)
continue
@@ -163,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'`,
)
@@ -171,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
}
@@ -196,11 +256,37 @@ 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!"
notificationTemplate = "The application !name (!url) went !status!"
}
for _, app := range apps {
@@ -295,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)
@@ -314,6 +548,18 @@ func sendNotifications(message string) {
if n.DiscordWebhook.Valid {
sendDiscord(n, message)
}
case "gotify":
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
sendGotify(n, message)
}
case "ntfy":
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
sendNtfy(n, message)
}
case "pushover":
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
sendPushover(n, message)
}
}
}
}
@@ -371,3 +617,96 @@ func sendDiscord(n Notification, message string) {
}
resp.Body.Close()
}
func sendGotify(n Notification, message string) {
baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/")
targetURL := fmt.Sprintf("%s/message", baseURL)
form := url.Values{}
form.Add("message", message)
form.Add("priority", "5")
req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Gotify: ERROR creating request: %v\n", err)
return
}
req.Header.Set("X-Gotify-Key", n.GotifyToken.String)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Gotify: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode)
}
}
func sendNtfy(n Notification, message string) {
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
topic := "corecontrol"
requestURL := fmt.Sprintf("%s/%s", baseURL, topic)
payload := map[string]string{"message": message}
jsonData, err := json.Marshal(payload)
if err != nil {
fmt.Printf("Ntfy: ERROR marshaling JSON: %v\n", err)
return
}
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("Ntfy: ERROR creating request: %v\n", err)
return
}
if n.NtfyToken.Valid {
req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Ntfy: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
}
}
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

@@ -3,7 +3,19 @@ import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
try {
const serverCount = await prisma.server.count();
const serverCountNoVMs = await prisma.server.count({
where: {
hostServer: 0
}
});
const serverCountOnlyVMs = await prisma.server.count({
where: {
hostServer: {
not: 0
}
}
});
const applicationCount = await prisma.application.count();
@@ -12,7 +24,8 @@ export async function POST(request: NextRequest) {
});
return NextResponse.json({
serverCount,
serverCountNoVMs,
serverCountOnlyVMs,
applicationCount,
onlineApplicationsCount
});

View File

@@ -30,6 +30,8 @@ interface Server {
id: number;
name: string;
ip: string;
host: boolean;
hostServer: number | null;
}
interface Application {
@@ -43,11 +45,15 @@ const NODE_WIDTH = 220;
const NODE_HEIGHT = 60;
const APP_NODE_WIDTH = 160;
const APP_NODE_HEIGHT = 40;
const HORIZONTAL_SPACING = 280;
const VERTICAL_SPACING = 60;
const HORIZONTAL_SPACING = 700;
const VERTICAL_SPACING = 80;
const START_Y = 120;
const ROOT_NODE_WIDTH = 300;
const CONTAINER_PADDING = 40;
const COLUMN_SPACING = 220;
const VM_APP_SPACING = 220;
const MIN_VM_SPACING = 10;
const APP_ROW_SPACING = 15;
export async function GET() {
try {
@@ -60,74 +66,138 @@ export async function GET() {
}) as Promise<Application[]>,
]);
// Root Node
const rootNode: Node = {
id: "root",
type: "infrastructure",
data: { label: "My Infrastructure" },
position: { x: 0, y: 0 },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "8px",
padding: "16px",
width: ROOT_NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "1.2rem",
fontWeight: "bold",
},
};
// Level 2: Physical Servers
const serverNodes: Node[] = servers
.filter(server => !server.hostServer)
.map((server, index, filteredServers) => {
const xPos =
index * HORIZONTAL_SPACING -
((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2;
// Server Nodes
const serverNodes: Node[] = servers.map((server, index) => {
const xPos =
index * HORIZONTAL_SPACING -
((servers.length - 1) * HORIZONTAL_SPACING) / 2;
return {
id: `server-${server.id}`,
type: "server",
data: {
label: `${server.name}\n${server.ip}`,
...server,
},
position: { x: xPos, y: START_Y },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "4px",
padding: "8px",
width: NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "0.9rem",
lineHeight: "1.2",
whiteSpace: "pre-wrap",
},
};
});
return {
id: `server-${server.id}`,
type: "server",
data: {
label: `${server.name}\n${server.ip}`,
...server,
},
position: { x: xPos, y: START_Y },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "4px",
padding: "8px",
width: NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "0.9rem",
lineHeight: "1.2",
whiteSpace: "pre-wrap",
},
};
});
// Application Nodes
const appNodes: Node[] = [];
// Level 3: Services and VMs
const serviceNodes: Node[] = [];
const vmNodes: Node[] = [];
servers.forEach((server) => {
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
const serverX = serverNode?.position.x || 0;
const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2;
if (serverNode) {
const serverX = serverNode.position.x;
// Services (left column)
applications
.filter(app => app.serverId === server.id)
.forEach((app, appIndex) => {
serviceNodes.push({
id: `service-${app.id}`,
type: "service",
data: {
label: `${app.name}\n${app.localURL}`,
...app,
},
position: {
x: serverX - COLUMN_SPACING,
y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20),
},
style: {
background: "#f0f9ff",
color: "#0f0f0f",
border: "2px solid #60a5fa",
borderRadius: "4px",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
});
// VMs (middle column) mit dynamischem Abstand
const hostVMs = servers.filter(vm => vm.hostServer === server.id);
let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING;
hostVMs.forEach(vm => {
const appCount = applications.filter(app => app.serverId === vm.id).length;
vmNodes.push({
id: `vm-${vm.id}`,
type: "vm",
data: {
label: `${vm.name}\n${vm.ip}`,
...vm,
},
position: {
x: serverX,
y: currentY,
},
style: {
background: "#fef2f2",
color: "#0f0f0f",
border: "2px solid #fecaca",
borderRadius: "4px",
padding: "6px",
width: APP_NODE_WIDTH,
height: APP_NODE_HEIGHT,
fontSize: "0.8rem",
lineHeight: "1.1",
whiteSpace: "pre-wrap",
},
});
// Dynamischer Abstand basierend auf Anzahl Apps
const requiredSpace = appCount > 0
? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING))
: 0;
currentY += Math.max(
requiredSpace + MIN_VM_SPACING,
MIN_VM_SPACING + APP_NODE_HEIGHT
);
});
}
});
// Level 4: VM Applications (right column)
const vmAppNodes: Node[] = [];
vmNodes.forEach((vm) => {
const vmX = vm.position.x;
applications
.filter((app) => app.serverId === server.id)
.filter(app => app.serverId === vm.data.id)
.forEach((app, appIndex) => {
appNodes.push({
id: `app-${app.id}`,
vmAppNodes.push({
id: `vm-app-${app.id}`,
type: "application",
data: {
label: `${app.name}\n${app.localURL}`,
...app,
},
position: {
x: serverX + xOffset,
y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING,
x: vmX + VM_APP_SPACING,
y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20),
},
style: {
background: "#f5f5f5",
@@ -145,38 +215,14 @@ export async function GET() {
});
});
// Connections
const connections: Edge[] = [
...servers.map((server) => ({
id: `conn-root-${server.id}`,
source: "root",
target: `server-${server.id}`,
type: "straight",
style: {
stroke: "#94a3b8",
strokeWidth: 2,
},
})),
...applications.map((app) => ({
id: `conn-${app.serverId}-${app.id}`,
source: `server-${app.serverId}`,
target: `app-${app.id}`,
type: "straight",
style: {
stroke: "#60a5fa",
strokeWidth: 2,
},
})),
];
// Container Box
const allNodes = [rootNode, ...serverNodes, ...appNodes];
// Calculate dimensions for root node positioning
const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes];
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
allNodes.forEach((node) => {
tempNodes.forEach((node) => {
const width = parseInt(node.style.width?.toString() || "0", 10);
const height = parseInt(node.style.height?.toString() || "0", 10);
@@ -186,17 +232,47 @@ export async function GET() {
maxY = Math.max(maxY, node.position.y + height);
});
const centerX = (minX + maxX) / 2;
const rootX = centerX - ROOT_NODE_WIDTH / 2;
// Level 1: Root Node (centered at top)
const rootNode: Node = {
id: "root",
type: "infrastructure",
data: { label: "My Infrastructure" },
position: { x: rootX, y: 0 },
style: {
background: "#ffffff",
color: "#0f0f0f",
border: "2px solid #e6e4e1",
borderRadius: "8px",
padding: "16px",
width: ROOT_NODE_WIDTH,
height: NODE_HEIGHT,
fontSize: "1.2rem",
fontWeight: "bold",
},
};
// Update dimensions with root node
const allNodes = [rootNode, ...tempNodes];
let newMinX = Math.min(minX, rootNode.position.x);
let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH);
let newMinY = Math.min(minY, rootNode.position.y);
let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT);
// Container Node
const containerNode: Node = {
id: 'container',
type: 'container',
data: { label: '' },
position: {
x: minX - CONTAINER_PADDING,
y: minY - CONTAINER_PADDING
x: newMinX - CONTAINER_PADDING,
y: newMinY - CONTAINER_PADDING
},
style: {
width: maxX - minX + 2 * CONTAINER_PADDING,
height: maxY - minY + 2 * CONTAINER_PADDING,
width: newMaxX - newMinX + 2 * CONTAINER_PADDING,
height: newMaxY - newMinY + 2 * CONTAINER_PADDING,
background: 'transparent',
border: '2px dashed #e2e8f0',
borderRadius: '8px',
@@ -207,6 +283,116 @@ export async function GET() {
zIndex: -1,
};
// Connections with hierarchical chaining
const connections: Edge[] = [];
// Root to Servers
serverNodes.forEach((server) => {
connections.push({
id: `conn-root-${server.id}`,
source: "root",
target: server.id,
type: "straight",
style: {
stroke: "#94a3b8",
strokeWidth: 2,
},
});
});
// Services chaining
const servicesByServer = new Map<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({
nodes: [containerNode, ...allNodes],
edges: connections,

View File

@@ -13,12 +13,19 @@ interface AddRequest {
telegramToken?: string;
telegramChatId?: string;
discordWebhook?: string;
gotifyUrl?: string;
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 } = 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: {
@@ -33,6 +40,13 @@ export async function POST(request: NextRequest) {
telegramChatId: telegramChatId,
telegramToken: telegramToken,
discordWebhook: discordWebhook,
gotifyUrl: gotifyUrl,
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken,
pushoverUser: pushoverUser,
}
});

View File

@@ -5,6 +5,7 @@ interface AddRequest {
host: boolean;
hostServer: number;
name: string;
icon: string;
os: string;
ip: string;
url: string;
@@ -12,26 +13,31 @@ 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, 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: {
host,
hostServer,
name,
icon,
os,
ip,
url,
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

@@ -6,6 +6,7 @@ interface EditRequest {
hostServer: number;
id: number;
name: string;
icon: string;
os: string;
ip: string;
url: string;
@@ -13,31 +14,43 @@ 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, 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) {
return NextResponse.json({ error: "Server not found" }, { status: 404 });
}
let newHostServer = hostServer;
if (hostServer === null) {
newHostServer = 0;
} else {
newHostServer = hostServer;
}
const updatedServer = await prisma.server.update({
where: { id },
data: {
host,
hostServer,
hostServer: newHostServer,
name,
icon,
os,
ip,
url,
cpu,
gpu,
ram,
disk
disk,
monitoring,
monitoringURL
}
});

View File

@@ -20,17 +20,29 @@ export async function POST(request: NextRequest) {
});
const hostsWithVms = await Promise.all(
hosts.map(async (host) => ({
...host,
hostedVMs: await prisma.server.findMany({
hosts.map(async (host) => {
const vms = await prisma.server.findMany({
where: { hostServer: host.id },
orderBy: { name: 'asc' }
})
}))
});
// Add isVM flag to VMs
const vmsWithFlag = vms.map(vm => ({
...vm,
isVM: true,
hostedVMs: [] // Initialize empty hostedVMs array for VMs
}));
return {
...host,
isVM: false, // Mark as physical server/not a VM
hostedVMs: vmsWithFlag
};
})
);
const totalHosts = await prisma.server.count({
where: { hostServer: null }
where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
});
const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);

View File

@@ -6,7 +6,15 @@ export async function GET(request: NextRequest) {
const servers = await prisma.server.findMany({
where: { host: true },
});
return NextResponse.json({ servers });
// Add required properties to ensure consistency
const serversWithProps = servers.map(server => ({
...server,
isVM: false,
hostedVMs: [] // Initialize empty hostedVMs array
}));
return NextResponse.json({ servers: serversWithProps });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}

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

@@ -11,15 +11,51 @@ export async function POST(request: NextRequest) {
const body: SearchRequest = await request.json();
const { searchterm } = body;
// Fetch all servers
const servers = await prisma.server.findMany({});
// Create a map of host servers with their hosted VMs
const serverMap = new Map();
servers.forEach(server => {
if (server.host) {
serverMap.set(server.id, {
...server,
isVM: false,
hostedVMs: []
});
}
});
// Add VMs to their host servers and mark them as VMs
const serversWithType = servers.map(server => {
// If not a host and has a hostServer, it's a VM
if (!server.host && server.hostServer) {
const hostServer = serverMap.get(server.hostServer);
if (hostServer) {
hostServer.hostedVMs.push({
...server,
isVM: true
});
}
return {
...server,
isVM: true
};
}
return {
...server,
isVM: false,
hostedVMs: serverMap.get(server.id)?.hostedVMs || []
};
});
const fuseOptions = {
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'],
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'],
threshold: 0.3,
includeScore: true,
};
const fuse = new Fuse(servers, fuseOptions);
const fuse = new Fuse(serversWithType, fuseOptions);
const searchResults = fuse.search(searchterm);

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

@@ -19,20 +19,23 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Button } from "@/components/ui/button"
interface StatsResponse {
serverCount: number
serverCountNoVMs: number
serverCountOnlyVMs: number
applicationCount: number
onlineApplicationsCount: number
}
export default function Dashboard() {
const [serverCount, setServerCount] = useState<number>(0)
const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
const [applicationCount, setApplicationCount] = useState<number>(0)
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
const getStats = async () => {
try {
const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
setServerCount(response.data.serverCount)
setServerCountNoVMs(response.data.serverCountNoVMs)
setServerCountOnlyVMs(response.data.serverCountOnlyVMs)
setApplicationCount(response.data.applicationCount)
setOnlineApplicationsCount(response.data.onlineApplicationsCount)
} catch (error: any) {
@@ -69,53 +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-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">Servers</CardTitle>
<Server className="h-6 w-6 text-rose-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 server infrastructure</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<div className="text-4xl font-bold">{serverCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active servers</p>
<CardContent 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/servers">View all servers</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-amber-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">Applications</CardTitle>
<Layers className="h-6 w-6 text-amber-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>Manage your deployed applications</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/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/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-sm transition-all hover:shadow-md">
<CardHeader className="pb-2">
<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">
<CardTitle className="text-xl font-medium">Uptime</CardTitle>
<Activity className="h-6 w-6 text-emerald-500" />
<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>
<CardDescription>Monitor your service availability</CardDescription>
</CardHeader>
<CardContent className="pt-2 pb-4">
<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>
@@ -136,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">
<div className="text-4xl font-bold">{serverCount + applicationCount}</div>
<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 ? (
@@ -479,7 +467,7 @@ export default function Dashboard() {
</CardDescription>
</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 flex-col space-y-2 flex-grow">
<Button
@@ -490,7 +478,7 @@ export default function Dashboard() {
}
>
<Link className="h-4 w-4" />
Open Public URL
Public URL
</Button>
{app.localURL && (
<Button
@@ -501,7 +489,7 @@ export default function Dashboard() {
}
>
<Home className="h-4 w-4" />
Open Local URL
Local URL
</Button>
)}
</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 {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { useTheme } from "next-themes";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { useTheme } from "next-themes"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Input } from "@/components/ui/input"
import { useEffect, useState } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react"
import axios from "axios"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle, Check, Palette, User, Bell } from "lucide-react";
import { AlertCircle, Check, Palette, User, Bell, AtSign, Send, MessageSquare, Trash2 } from "lucide-react"
import {
AlertDialog,
@@ -39,19 +31,20 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Textarea } from "@/components/ui/textarea"
interface NotificationsResponse {
notifications: any[];
notifications: any[]
}
interface NotificationResponse {
notification_text?: string;
notification_text_application?: string
notification_text_server?: string
}
export default function Settings() {
const { theme, setTheme } = useTheme();
const { theme, setTheme } = useTheme()
const [email, setEmail] = useState<string>("")
const [password, setPassword] = useState<string>("")
@@ -77,95 +70,102 @@ export default function Settings() {
const [telegramToken, setTelegramToken] = useState<string>("")
const [telegramChatId, setTelegramChatId] = 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 [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);
setEmailSuccess(false);
setEmailError("");
setEmailErrorVisible(false)
setEmailSuccess(false)
setEmailError("")
if (!email) {
setEmailError("Email is required");
setEmailErrorVisible(true);
setEmailError("Email is required")
setEmailErrorVisible(true)
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}
, 3000);
return;
setEmailErrorVisible(false)
setEmailError("")
}, 3000)
return
}
try {
await axios.post('/api/auth/edit_email', {
await axios.post("/api/auth/edit_email", {
newEmail: email,
jwtToken: Cookies.get('token')
});
setEmailSuccess(true);
setEmail("");
jwtToken: Cookies.get("token"),
})
setEmailSuccess(true)
setEmail("")
setTimeout(() => {
setEmailSuccess(false);
}, 3000);
setEmailSuccess(false)
}, 3000)
} catch (error: any) {
setEmailError(error.response.data.error);
setEmailErrorVisible(true);
setEmailError(error.response.data.error)
setEmailErrorVisible(true)
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}, 3000);
setEmailErrorVisible(false)
setEmailError("")
}, 3000)
}
}
const changePassword = async () => {
try {
if (password !== confirmPassword) {
setPasswordError("Passwords do not match");
setPasswordErrorVisible(true);
setPasswordError("Passwords do not match")
setPasswordErrorVisible(true)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
return
}
if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required");
setPasswordErrorVisible(true);
setPasswordError("All fields are required")
setPasswordErrorVisible(true)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
return
}
const response = await axios.post('/api/auth/edit_password', {
const response = await axios.post("/api/auth/edit_password", {
oldPassword: oldPassword,
newPassword: password,
jwtToken: Cookies.get('token')
});
jwtToken: Cookies.get("token"),
})
if (response.status === 200) {
setPasswordSuccess(true);
setPassword("");
setOldPassword("");
setConfirmPassword("");
setPasswordSuccess(true)
setPassword("")
setOldPassword("")
setConfirmPassword("")
setTimeout(() => {
setPasswordSuccess(false);
}, 3000);
setPasswordSuccess(false)
}, 3000)
}
} catch (error: any) {
setPasswordErrorVisible(true);
setPasswordError(error.response.data.error);
setPasswordErrorVisible(true)
setPasswordError(error.response.data.error)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
}
}
const addNotification = async () => {
try {
const response = await axios.post('/api/notifications/add', {
const response = await axios.post("/api/notifications/add", {
type: notificationType,
smtpHost: smtpHost,
smtpPort: smtpPort,
@@ -176,37 +176,42 @@ export default function Settings() {
smtpTo: smtpTo,
telegramToken: telegramToken,
telegramChatId: telegramChatId,
discordWebhook: discordWebhook
});
getNotifications();
}
catch (error: any) {
alert(error.response.data.error);
discordWebhook: discordWebhook,
gotifyUrl: gotifyUrl,
gotifyToken: gotifyToken,
ntfyUrl: ntfyUrl,
ntfyToken: ntfyToken,
pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken,
pushoverUser: pushoverUser,
})
getNotifications()
} catch (error: any) {
alert(error.response.data.error)
}
}
const deleteNotification = async (id: number) => {
try {
const response = await axios.post('/api/notifications/delete', {
id: id
});
const response = await axios.post("/api/notifications/delete", {
id: id,
})
if (response.status === 200) {
getNotifications()
}
} catch (error: any) {
alert(error.response.data.error);
alert(error.response.data.error)
}
}
const getNotifications = async () => {
try {
const response = await axios.post<NotificationsResponse>('/api/notifications/get', {});
const response = await axios.post<NotificationsResponse>("/api/notifications/get", {})
if (response.status === 200 && response.data) {
setNotifications(response.data.notifications);
setNotifications(response.data.notifications)
}
}
catch (error: any) {
alert(error.response.data.error);
} catch (error: any) {
alert(error.response.data.error)
}
}
@@ -214,29 +219,34 @@ export default function Settings() {
getNotifications()
}, [])
const getNotificationText = async () => {
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.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) {
alert(error.response.data.error);
alert(error.response.data.error)
}
};
}
const editNotificationText = async () => {
try {
const response = await axios.post('/api/settings/notification_text', {
text: notificationText
});
const response = await axios.post("/api/settings/notification_text", {
text_application: notificationTextApplication,
text_server: notificationTextServer,
})
} catch (error: any) {
alert(error.response.data.error);
alert(error.response.data.error)
}
}
@@ -403,209 +413,381 @@ export default function Settings() {
</CardContent>
</Card>
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<div className="flex items-center gap-3">
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
<h2 className="text-xl font-semibold">Notifications</h2>
</div>
</CardHeader>
<CardContent className="pb-6">
<CardContent className="p-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>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full">
Add Notification
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Add Notification</AlertDialogTitle>
<AlertDialogDescription>
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Notification Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="discord">Discord</SelectItem>
</SelectContent>
<div className="grid gap-4 md:grid-cols-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full h-11 flex items-center gap-2">
Add Notification Channel
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Add Notification</AlertDialogTitle>
<AlertDialogDescription>
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Notification Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="discord">Discord</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem>
</SelectContent>
{notificationType === "smtp" && (
<div className="mt-4 space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpHost">SMTP Host</Label>
<Input
type="text"
id="smtpHost"
placeholder="smtp.example.com"
onChange={(e) => setSmtpHost(e.target.value)}
/>
{notificationType === "smtp" && (
<div className="mt-4 space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpHost">SMTP Host</Label>
<Input
type="text"
id="smtpHost"
placeholder="smtp.example.com"
onChange={(e) => setSmtpHost(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPort">SMTP Port</Label>
<Input
type="number"
id="smtpPort"
placeholder="587"
onChange={(e) => setSmtpPort(Number(e.target.value))}
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL)
</Label>
</div>
<div className="grid gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpUser">SMTP Username</Label>
<Input
type="text"
id="smtpUser"
placeholder="user@example.com"
onChange={(e) => setSmtpUsername(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPass">SMTP Password</Label>
<Input
type="password"
id="smtpPass"
placeholder="••••••••"
onChange={(e) => setSmtpPassword(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpFrom">From Address</Label>
<Input
type="email"
id="smtpFrom"
placeholder="noreply@example.com"
onChange={(e) => setSmtpFrom(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpTo">To Address</Label>
<Input
type="email"
id="smtpTo"
placeholder="admin@example.com"
onChange={(e) => setSmtpTo(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPort">SMTP Port</Label>
<Input
type="number"
id="smtpPort"
placeholder="587"
onChange={(e) => setSmtpPort(Number(e.target.value))}
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox
id="smtpSecure"
onCheckedChange={(checked: any) => setSmtpSecure(checked)}
/>
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL)
</Label>
</div>
<div className="grid gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpUser">SMTP Username</Label>
<Input
type="text"
id="smtpUser"
placeholder="user@example.com"
onChange={(e) => setSmtpUsername(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPass">SMTP Password</Label>
<Input
type="password"
id="smtpPass"
placeholder="••••••••"
onChange={(e) => setSmtpPassword(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpFrom">From Address</Label>
)}
{notificationType === "telegram" && (
<div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label>
<Input
type="email"
id="smtpFrom"
placeholder="noreply@example.com"
onChange={(e) => setSmtpFrom(e.target.value)}
type="text"
id="telegramToken"
placeholder=""
onChange={(e) => setTelegramToken(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpTo">To Address</Label>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label>
<Input
type="email"
id="smtpTo"
placeholder="admin@example.com"
onChange={(e) => setSmtpTo(e.target.value)}
type="text"
id="telegramChatId"
placeholder=""
onChange={(e) => setTelegramChatId(e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
)}
{notificationType === "telegram" && (
<div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label>
<Input type="text" id="telegramToken" placeholder="" onChange={(e) => setTelegramToken(e.target.value)} />
{notificationType === "discord" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label>
<Input
type="text"
id="discordWebhook"
placeholder=""
onChange={(e) => setDiscordWebhook(e.target.value)}
/>
</div>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label>
<Input type="text" id="telegramChatId" placeholder="" onChange={(e) => setTelegramChatId(e.target.value)} />
)}
{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>
</div>
)}
)}
{notificationType === "discord" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label>
<Input type="text" id="discordWebhook" placeholder="" onChange={(e) => setDiscordWebhook(e.target.value)} />
{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>
</div>
)}
)}
</Select>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}>
Add
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{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>
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="pt-4 pb-2">
<Button className="w-full" variant="secondary">
<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>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full h-11" variant="outline">
Customize Notification Text
</Button>
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
<AlertDialogDescription>
<div className="space-y-4">
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
<AlertDialogDescription>
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="text">Notification Text</Label>
<Textarea id="text" placeholder="Type here..." value={notificationText} onChange={(e) => setNotificationText(e.target.value)} rows={4} />
<Label htmlFor="text_application">Notification Text for Applications</Label>
<Textarea
id="text_application"
placeholder="Type here..."
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>
</div>
<div className="pt-4 text-sm text-muted-foreground">
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</li>
<li><strong>!url</strong> - Application URL</li>
<li><strong>!status</strong> - Application status (online/offline)</li>
</ul>
</div>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<div className="mt-6 space-y-4">
{notifications.length > 0 ? (
notifications.map((notification) => (
<div
key={notification.id}
className="flex items-center justify-between p-4 bg-muted/10 rounded-lg border"
>
<div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => deleteNotification(notification.id)}
<div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2">
<li>
<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>
<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>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<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.map((notification) => (
<div
key={notification.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card transition-all hover:shadow-sm"
>
Delete
</Button>
<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>
)}
{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">
{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"}
{notification.type === "pushover" && "Pushover notifications"}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="hover:bg-muted/20"
onClick={() => deleteNotification(notification.id)}
>
<Trash2 className="h-4 w-4 mr-1" />
Remove
</Button>
</div>
))
) : (
<div className="text-center py-12 border rounded-lg bg-muted/5">
<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 className="text-center text-muted-foreground py-6">
No notifications configured
</div>
)}
)}
</div>
</div>
</CardContent>
</Card>

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

@@ -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 }

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,64 @@
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: 'General', 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' }
]
}
})

23
docs/.vitepress/dist/404.html vendored Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en-US" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>404 | CoreControl</title>
<meta name="description" content="Not Found">
<meta name="generator" content="VitePress v1.6.3">
<link rel="preload stylesheet" href="/assets/style.DEOyzpKL.css" as="style">
<link rel="preload stylesheet" href="/vp-icons.css" as="style">
<script type="module" src="/assets/chunks/metadata.87c7e30c.js"></script>
<script type="module" src="/assets/app.CZwi0YgD.js"></script>
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="icon" type="image/png" href="/logo.png">
<script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
<script id="check-mac-os">document.documentElement.classList.toggle("mac",/Mac|iPhone|iPod|iPad/i.test(navigator.platform));</script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
import{t as p}from"./chunks/theme.CkdfpqM_.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.DPDPlp3K.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp};

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
window.__VP_HASH_MAP__=JSON.parse("{\"general_applications.md\":\"DFVqSlCw\",\"general_dashboard.md\":\"DW5yESFW\",\"general_network.md\":\"tbP8aEzX\",\"general_servers.md\":\"BaASA60T\",\"general_settings.md\":\"DrC2XV32\",\"general_uptime.md\":\"CKBdQg4u\",\"index.md\":\"_yXl4OkC\",\"installation.md\":\"Cz1eOHOr\",\"notifications_discord.md\":\"C0x5CxmR\",\"notifications_email.md\":\"Cugw2BRs\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"vFHjr6ko\",\"notifications_ntfy.md\":\"CPMnGQVP\",\"notifications_telegram.md\":\"B6_EzaEX\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"CoreControl\",\"description\":\"Dashboard to manage your entire server infrastructure\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"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\":\"General\",\"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\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");

View File

@@ -0,0 +1 @@
const s="/assets/settings_notifications.DwqFpmxq.png";export{s as _};

File diff suppressed because one or more lines are too long

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

View File

@@ -0,0 +1 @@
import{_ as i,c as a,o,ag as e}from"./chunks/framework.DPDPlp3K.js";const l="/assets/applications_add_button.CTBM75AA.png",n="/assets/applications_display.D1ZCmp63.png",_=JSON.parse('{"title":"Applications","description":"","frontmatter":{},"headers":[],"relativePath":"general/Applications.md","filePath":"general/Applications.md","lastUpdated":1745241280000}'),p={name:"general/Applications.md"};function r(s,t,c,d,h,u){return o(),a("div",null,t[0]||(t[0]=[e('<h1 id="applications" tabindex="-1">Applications <a class="header-anchor" href="#applications" aria-label="Permalink to &quot;Applications&quot;"></a></h1><p>All your self-hosted applications are displayed here.</p><h2 id="add-an-application" tabindex="-1">Add an application <a class="header-anchor" href="#add-an-application" aria-label="Permalink to &quot;Add an application&quot;"></a></h2><p>To add a new application to CoreControl, follow these steps:</p><ol><li><p>Click the &quot;Add Application&quot; button in the top right corner of the server menu: <img src="'+l+'" alt="Application Add Button"></p></li><li><p>Fill out the server details across the following information:</p></li></ol><ul><li><strong>Name</strong>: Enter the name of the application</li><li><strong>Server</strong>: Select the server on which the application is running</li><li><strong>Description</strong>: Enter a short (or long) description of the server</li><li><strong>Icon URL</strong>: Add the url pointing to the logo of the application. With the flash button the logo will be automatically selected.</li><li><strong>Public URL</strong>: Enter the public URL of your application. This will be used to track the uptime.</li><li><strong>Local URL</strong>: Enter the local URL of your application, i.e. the URL via which the application is only accessible in the local network</li></ul><p>After filling out the required information, click &quot;Add&quot; to add the application to CoreControl.</p><h2 id="application-display" tabindex="-1">Application Display <a class="header-anchor" href="#application-display" aria-label="Permalink to &quot;Application Display&quot;"></a></h2><p>Your applications are displayed in a list or grid (depending on the display settings) - each application in its own card <img src="'+n+'" alt="Application card"></p>',9)]))}const f=i(p,[["render",r]]);export{_ as __pageData,f as default};

View File

@@ -0,0 +1 @@
import{_ as i,c as a,o,ag as e}from"./chunks/framework.DPDPlp3K.js";const l="/assets/applications_add_button.CTBM75AA.png",n="/assets/applications_display.D1ZCmp63.png",_=JSON.parse('{"title":"Applications","description":"","frontmatter":{},"headers":[],"relativePath":"general/Applications.md","filePath":"general/Applications.md","lastUpdated":1745241280000}'),p={name:"general/Applications.md"};function r(s,t,c,d,h,u){return o(),a("div",null,t[0]||(t[0]=[e("",9)]))}const f=i(p,[["render",r]]);export{_ as __pageData,f as default};

View File

@@ -0,0 +1 @@
import{_ as r,c as e,o as s,ag as t}from"./chunks/framework.DPDPlp3K.js";const o="/assets/dashboard_card_servers.DNNhRbkY.png",i="/assets/dashboard_card_applications.DErIEBeJ.png",d="/assets/dashboard_card_uptime.B6cdZ6Ju.png",l="/assets/dashboard_card_network.C2YTU2ur.png",f=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"general/Dashboard.md","filePath":"general/Dashboard.md","lastUpdated":1745241280000}'),n={name:"general/Dashboard.md"};function c(p,a,h,u,m,_){return s(),e("div",null,a[0]||(a[0]=[t('<h1 id="dashboard" tabindex="-1">Dashboard <a class="header-anchor" href="#dashboard" aria-label="Permalink to &quot;Dashboard&quot;"></a></h1><p>The dashboard is the most important place to get a quick overview of your infrastructure.</p><h2 id="cards-overview" tabindex="-1">Cards Overview <a class="header-anchor" href="#cards-overview" aria-label="Permalink to &quot;Cards Overview&quot;"></a></h2><p>The dashboard is divided into 4 cards that provide different aspects of your infrastructure monitoring:</p><h3 id="servers-card" tabindex="-1">Servers Card <a class="header-anchor" href="#servers-card" aria-label="Permalink to &quot;Servers Card&quot;"></a></h3><p><img src="'+o+'" alt="Servers Card"></p><p>The Servers card displays information about all your connected servers, including:</p><ul><li>Number of Physical Servers</li><li>Number of Virtual Servers</li></ul><h3 id="applications-card" tabindex="-1">Applications Card <a class="header-anchor" href="#applications-card" aria-label="Permalink to &quot;Applications Card&quot;"></a></h3><p><img src="'+i+'" alt="Applications Card"></p><p>The Applications card shows you:</p><ul><li>Number of running applications across your infrastructure</li></ul><h3 id="uptime-card" tabindex="-1">Uptime Card <a class="header-anchor" href="#uptime-card" aria-label="Permalink to &quot;Uptime Card&quot;"></a></h3><p><img src="'+d+'" alt="Uptime Card"></p><p>The Uptime card provides:</p><ul><li>Number of online applications</li></ul><h3 id="network-card" tabindex="-1">Network Card <a class="header-anchor" href="#network-card" aria-label="Permalink to &quot;Network Card&quot;"></a></h3><p><img src="'+l+'" alt="Network Card"></p><p>The Network card displays:</p><ul><li>Sum of servers and applications</li></ul>',20)]))}const v=r(n,[["render",c]]);export{f as __pageData,v as default};

View File

@@ -0,0 +1 @@
import{_ as r,c as e,o as s,ag as t}from"./chunks/framework.DPDPlp3K.js";const o="/assets/dashboard_card_servers.DNNhRbkY.png",i="/assets/dashboard_card_applications.DErIEBeJ.png",d="/assets/dashboard_card_uptime.B6cdZ6Ju.png",l="/assets/dashboard_card_network.C2YTU2ur.png",f=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"general/Dashboard.md","filePath":"general/Dashboard.md","lastUpdated":1745241280000}'),n={name:"general/Dashboard.md"};function c(p,a,h,u,m,_){return s(),e("div",null,a[0]||(a[0]=[t("",20)]))}const v=r(n,[["render",c]]);export{f as __pageData,v as default};

View File

@@ -0,0 +1 @@
import{_ as r,c as a,o as n,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Network","description":"","frontmatter":{},"headers":[],"relativePath":"general/Network.md","filePath":"general/Network.md","lastUpdated":1745241280000}'),s={name:"general/Network.md"};function i(l,t,c,d,h,p){return n(),a("div",null,t[0]||(t[0]=[e("h1",{id:"network",tabindex:"-1"},[o("Network "),e("a",{class:"header-anchor",href:"#network","aria-label":'Permalink to "Network"'},"")],-1),e("p",null,"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.",-1)]))}const w=r(s,[["render",i]]);export{u as __pageData,w as default};

View File

@@ -0,0 +1 @@
import{_ as r,c as a,o as n,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Network","description":"","frontmatter":{},"headers":[],"relativePath":"general/Network.md","filePath":"general/Network.md","lastUpdated":1745241280000}'),s={name:"general/Network.md"};function i(l,t,c,d,h,p){return n(),a("div",null,t[0]||(t[0]=[e("h1",{id:"network",tabindex:"-1"},[o("Network "),e("a",{class:"header-anchor",href:"#network","aria-label":'Permalink to "Network"'},"")],-1),e("p",null,"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.",-1)]))}const w=r(s,[["render",i]]);export{u as __pageData,w as default};

View File

@@ -0,0 +1,12 @@
import{_ as s,c as a,o as i,ag as t}from"./chunks/framework.DPDPlp3K.js";const r="/assets/servers_add_button.DqYHhWPq.png",n="/assets/servers_display.f8nBpTOs.png",l="/assets/servers_vms_button.CXHECWPE.png",o="/assets/servers_vms_list.C3B4ERR1.png",v=JSON.parse('{"title":"Servers","description":"","frontmatter":{},"headers":[],"relativePath":"general/Servers.md","filePath":"general/Servers.md","lastUpdated":1745241280000}'),h={name:"general/Servers.md"};function p(d,e,c,g,k,u){return i(),a("div",null,e[0]||(e[0]=[t('<h1 id="servers" tabindex="-1">Servers <a class="header-anchor" href="#servers" aria-label="Permalink to &quot;Servers&quot;"></a></h1><p>In the server menu you can see all your servers and add more if required</p><h2 id="add-a-server" tabindex="-1">Add a Server <a class="header-anchor" href="#add-a-server" aria-label="Permalink to &quot;Add a Server&quot;"></a></h2><p>To add a new server to CoreControl, follow these steps:</p><ol><li><p>Click the &quot;Add Server&quot; button in the top right corner of the server menu: <img src="'+r+`" alt="Servers Add Button"></p></li><li><p>Fill out the server details across the following tabs:</p></li></ol><h3 id="general-tab" tabindex="-1">General Tab <a class="header-anchor" href="#general-tab" aria-label="Permalink to &quot;General Tab&quot;"></a></h3><p>Configure the basic server information:</p><ul><li><strong>Icon</strong>: Choose a custom icon for your server</li><li><strong>Name</strong>: Enter a descriptive name for the server</li><li><strong>Operating System</strong>: Select the server&#39;s operating system</li><li><strong>IP Address</strong>: Enter the server&#39;s IP address</li><li><strong>Management URL</strong>: Add the URL used to manage the server (optional)</li></ul><h3 id="hardware-tab" tabindex="-1">Hardware Tab <a class="header-anchor" href="#hardware-tab" aria-label="Permalink to &quot;Hardware Tab&quot;"></a></h3><p>Specify the server&#39;s hardware specifications:</p><ul><li><strong>CPU</strong>: Enter CPU model and specifications</li><li><strong>GPU</strong>: Add graphics card details if applicable</li><li><strong>RAM</strong>: Specify the amount of RAM</li><li><strong>Disk</strong>: Enter storage capacity and configuration</li></ul><h3 id="virtualization-tab" tabindex="-1">Virtualization Tab <a class="header-anchor" href="#virtualization-tab" aria-label="Permalink to &quot;Virtualization Tab&quot;"></a></h3><p>Configure virtualization settings:</p><ul><li><strong>Host Server Settings</strong>: <ul><li>Enable &quot;Host Server&quot; if this server will host virtual machines</li><li>Perfect for hypervisors like Proxmox, VMware, or similar</li></ul></li><li><strong>VM Settings</strong>: <ul><li>Select a host server if this server is a virtual machine</li><li>This creates a logical connection between the VM and its host</li></ul></li></ul><h3 id="monitoring-tab" tabindex="-1">Monitoring Tab <a class="header-anchor" href="#monitoring-tab" aria-label="Permalink to &quot;Monitoring Tab&quot;"></a></h3><p>Set up server monitoring options (see &quot;Monitoring&quot; section for detailed information)</p><p>After filling out the required information, click &quot;Add&quot; to add the server to CoreControl.</p><h2 id="monitoring" tabindex="-1">Monitoring <a class="header-anchor" href="#monitoring" aria-label="Permalink to &quot;Monitoring&quot;"></a></h2><p>If you want to monitor the hardware usage and status of your servers, you will have to enable monitoring in the monitoring tab.</p><p>After you have done this you need to install <a href="https://github.com/nicolargo/glances" target="_blank" rel="noreferrer">Glances</a> 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 <a href="https://glances.readthedocs.io/en/latest/" target="_blank" rel="noreferrer">Glances docs</a>.</p><div class="language-yaml vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">services</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> glances</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">nicolargo/glances:latest</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> container_name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">glances</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> restart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">unless-stopped</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> ports</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;61208:61208&quot;</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> pid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;host&quot;</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">/var/run/docker.sock:/var/run/docker.sock:ro</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">GLANCES_OPT=-w --disable-webui</span></span></code></pre></div><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>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 <code>http://&lt;IP_OF_SERVER&gt;:61208</code>.</p></div><h2 id="server-display" tabindex="-1">Server Display <a class="header-anchor" href="#server-display" aria-label="Permalink to &quot;Server Display&quot;"></a></h2><p>Your servers are displayed in a list or grid (depending on the display settings) - each server in its own card <img src="`+n+'" alt="Server Card"></p><p>There are also three action buttons at the end of each card.</p><ul><li>Link Button - With this you can open the specified management URL of the server with one click</li><li>Delete Button - Direct deletion of the server</li><li>Edit Button - Customize the server with the same menu as when creating the server</li></ul><h2 id="vms" tabindex="-1">VMs <a class="header-anchor" href="#vms" aria-label="Permalink to &quot;VMs&quot;"></a></h2><p>If a host server contains VMs, you can display them using the “VMs” button <img src="'+l+'" alt="VMs Button"></p><p>The associated VMs are then displayed in a clearly arranged list. <img src="'+o+'" alt="VM List"></p>',29)]))}const E=s(h,[["render",p]]);export{v as __pageData,E as default};

View File

@@ -0,0 +1 @@
import{_ as s,c as a,o as i,ag as t}from"./chunks/framework.DPDPlp3K.js";const r="/assets/servers_add_button.DqYHhWPq.png",n="/assets/servers_display.f8nBpTOs.png",l="/assets/servers_vms_button.CXHECWPE.png",o="/assets/servers_vms_list.C3B4ERR1.png",v=JSON.parse('{"title":"Servers","description":"","frontmatter":{},"headers":[],"relativePath":"general/Servers.md","filePath":"general/Servers.md","lastUpdated":1745241280000}'),h={name:"general/Servers.md"};function p(d,e,c,g,k,u){return i(),a("div",null,e[0]||(e[0]=[t("",29)]))}const E=s(h,[["render",p]]);export{v as __pageData,E as default};

View File

@@ -0,0 +1 @@
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as s,c as a,o as i,ag as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/settings_user.eib6RZVK.png",o="/assets/settings_theme.AZP0Uw0g.png",f=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745241280000}'),g={name:"general/Settings.md"};function l(c,t,_,h,m,p){return i(),a("div",null,t[0]||(t[0]=[n('<h1 id="settings" tabindex="-1">Settings <a class="header-anchor" href="#settings" aria-label="Permalink to &quot;Settings&quot;"></a></h1><p>Here you can manage the complete settings of CoreControl.</p><h2 id="user-settings" tabindex="-1">User Settings <a class="header-anchor" href="#user-settings" aria-label="Permalink to &quot;User Settings&quot;"></a></h2><p><img src="'+r+'" alt="User Settings"></p><h2 id="theme-settings" tabindex="-1">Theme Settings <a class="header-anchor" href="#theme-settings" aria-label="Permalink to &quot;Theme Settings&quot;"></a></h2><p><img src="'+o+'" alt="Theme Settings"></p><h2 id="notification-settings" tabindex="-1">Notification Settings <a class="header-anchor" href="#notification-settings" aria-label="Permalink to &quot;Notification Settings&quot;"></a></h2><p><img src="'+e+'" alt="Notification Settings"></p>',8)]))}const u=s(g,[["render",l]]);export{f as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as s,c as a,o as i,ag as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/settings_user.eib6RZVK.png",o="/assets/settings_theme.AZP0Uw0g.png",f=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745241280000}'),g={name:"general/Settings.md"};function l(c,t,_,h,m,p){return i(),a("div",null,t[0]||(t[0]=[n("",8)]))}const u=s(g,[["render",l]]);export{f as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as a,c as i,o as s,j as e,a as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/uptime.Dt6hpqNV.png",f=JSON.parse('{"title":"Uptime","description":"","frontmatter":{},"headers":[],"relativePath":"general/Uptime.md","filePath":"general/Uptime.md","lastUpdated":1745241280000}'),p={name:"general/Uptime.md"};function l(o,t,m,c,d,u){return s(),i("div",null,t[0]||(t[0]=[e("h1",{id:"uptime",tabindex:"-1"},[n("Uptime "),e("a",{class:"header-anchor",href:"#uptime","aria-label":'Permalink to "Uptime"'},"")],-1),e("p",null,"The uptime of all your Applications is shown here in a clear list.",-1),e("p",null,[e("img",{src:r,alt:"Uptime"})],-1),e("p",null,"With the Select menu you can also filter the time span (30min, 7 days and 30 days)",-1)]))}const _=a(p,[["render",l]]);export{f as __pageData,_ as default};

View File

@@ -0,0 +1 @@
import{_ as a,c as i,o as s,j as e,a as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/uptime.Dt6hpqNV.png",f=JSON.parse('{"title":"Uptime","description":"","frontmatter":{},"headers":[],"relativePath":"general/Uptime.md","filePath":"general/Uptime.md","lastUpdated":1745241280000}'),p={name:"general/Uptime.md"};function l(o,t,m,c,d,u){return s(),i("div",null,t[0]||(t[0]=[e("h1",{id:"uptime",tabindex:"-1"},[n("Uptime "),e("a",{class:"header-anchor",href:"#uptime","aria-label":'Permalink to "Uptime"'},"")],-1),e("p",null,"The uptime of all your Applications is shown here in a clear list.",-1),e("p",null,[e("img",{src:r,alt:"Uptime"})],-1),e("p",null,"With the Select menu you can also filter the time span (30min, 7 days and 30 days)",-1)]))}const _=a(p,[["render",l]]);export{f as __pageData,_ as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"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"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"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"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};

View File

@@ -0,0 +1,36 @@
import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Installation","description":"","frontmatter":{},"headers":[],"relativePath":"installation.md","filePath":"installation.md","lastUpdated":1745171698000}'),l={name:"installation.md"};function e(p,s,h,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="installation" tabindex="-1">Installation <a class="header-anchor" href="#installation" aria-label="Permalink to &quot;Installation&quot;"></a></h1><p>The easiest way to install CoreControl is using Docker Compose. Follow these steps:</p><h2 id="docker-compose-installation" tabindex="-1">Docker Compose Installation <a class="header-anchor" href="#docker-compose-installation" aria-label="Permalink to &quot;Docker Compose Installation&quot;"></a></h2><div class="danger custom-block"><p class="custom-block-title">DANGER</p><p>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.</p></div><ol><li><p>Make sure <a href="https://docs.docker.com/get-docker/" target="_blank" rel="noreferrer">Docker</a> and <a href="https://docs.docker.com/compose/install/" target="_blank" rel="noreferrer">Docker Compose</a> are installed on your system.</p></li><li><p>Create a file named <code>docker-compose.yml</code> with the following content:</p></li></ol><div class="language-yaml vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">services</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> web</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">haedlessdev/corecontrol:latest</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> ports</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;3000:3000&quot;</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> JWT_SECRET</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">RANDOM_SECRET</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Replace with a secure random string</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> DATABASE_URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;postgresql://postgres:postgres@db:5432/postgres&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> agent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">haedlessdev/corecontrol-agent:latest</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> DATABASE_URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;postgresql://postgres:postgres@db:5432/postgres&quot;</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> depends_on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> db</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> condition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">service_healthy</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> db</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres:17</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> restart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">always</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_USER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_PASSWORD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres_data:/var/lib/postgresql/data</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> healthcheck</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> test</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;CMD-SHELL&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;pg_isready -U postgres&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> interval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">2s</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> timeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">2s</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> retries</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">10</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> postgres_data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span></code></pre></div><ol start="3"><li>Generate a custom JWT_SECRET with e.g. <a href="https://jwtsecret.com/generate" target="_blank" rel="noreferrer">jwtsecret.com/generate</a></li><li>Start CoreControl with the following command:</li></ol><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker-compose</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> up</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -d</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># OR</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> compose</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> up</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -d</span></span></code></pre></div><ol start="5"><li>The application is now available at <code>http://localhost:3000</code>.</li></ol><h2 id="authentication" tabindex="-1">Authentication <a class="header-anchor" href="#authentication" aria-label="Permalink to &quot;Authentication&quot;"></a></h2><p>CoreControl comes with a default administrator account:</p><ul><li><strong>Email</strong>: <a href="mailto:admin@example.com" target="_blank" rel="noreferrer">admin@example.com</a></li><li><strong>Password</strong>: admin</li></ul><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>For security reasons, it is strongly recommended to change the default credentials immediately after your first login.</p></div><p>You can change the administrator password in the settings after logging in.</p>`,14)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};

View File

@@ -0,0 +1 @@
import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Installation","description":"","frontmatter":{},"headers":[],"relativePath":"installation.md","filePath":"installation.md","lastUpdated":1745171698000}'),l={name:"installation.md"};function e(p,s,h,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t("",14)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};

View File

@@ -0,0 +1 @@
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as i}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as o,c as n,o as a,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const _=JSON.parse('{"title":"Notifications","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/General.md","filePath":"notifications/General.md","lastUpdated":1745241280000}'),r={name:"notifications/General.md"};function c(l,e,f,d,p,m){return a(),n("div",null,e[0]||(e[0]=[t("h1",{id:"notifications",tabindex:"-1"},[s("Notifications "),t("a",{class:"header-anchor",href:"#notifications","aria-label":'Permalink to "Notifications"'},"")],-1),t("p",null,"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.",-1),t("p",null,[t("img",{src:i,alt:"Notification Settings"})],-1),t("p",null,"You can also customize direct notification texts and improve them with placeholders",-1)]))}const N=o(r,[["render",c]]);export{_ as __pageData,N as default};

View File

@@ -0,0 +1 @@
import{_ as i}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as o,c as n,o as a,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const _=JSON.parse('{"title":"Notifications","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/General.md","filePath":"notifications/General.md","lastUpdated":1745241280000}'),r={name:"notifications/General.md"};function c(l,e,f,d,p,m){return a(),n("div",null,e[0]||(e[0]=[t("h1",{id:"notifications",tabindex:"-1"},[s("Notifications "),t("a",{class:"header-anchor",href:"#notifications","aria-label":'Permalink to "Notifications"'},"")],-1),t("p",null,"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.",-1),t("p",null,[t("img",{src:i,alt:"Notification Settings"})],-1),t("p",null,"You can also customize direct notification texts and improve them with placeholders",-1)]))}const N=o(r,[["render",c]]);export{_ as __pageData,N as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};

View File

@@ -0,0 +1 @@
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};

View File

@@ -0,0 +1 @@
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};

View File

@@ -0,0 +1 @@
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};

View File

@@ -0,0 +1 @@
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};

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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More