mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-29 17:14:43 +01:00
v0.0.10 -> main
V0.0.10
This commit is contained in:
Binary file not shown.
+22
-22
@@ -1,25 +1,25 @@
|
||||
# --- Build Stage ---
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.19-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app .
|
||||
|
||||
# --- Run Stage ---
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/app .
|
||||
|
||||
CMD ["./app"]
|
||||
WORKDIR /app
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app ./cmd/agent
|
||||
|
||||
# --- Run Stage ---
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/app .
|
||||
|
||||
CMD ["./app"]
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/app"
|
||||
"github.com/corecontrol/agent/internal/database"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
"github.com/corecontrol/agent/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize database
|
||||
db, err := database.InitDB()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Database initialization failed: %v\n", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Initialize notification sender
|
||||
notifSender := notifications.NewNotificationSender()
|
||||
|
||||
// Initial load of notifications
|
||||
notifs, err := database.LoadNotifications(db)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load notifications: %v", err))
|
||||
}
|
||||
notifSender.UpdateNotifications(notifs)
|
||||
|
||||
// Reload notification configs every minute
|
||||
go func() {
|
||||
reloadTicker := time.NewTicker(time.Minute)
|
||||
defer reloadTicker.Stop()
|
||||
|
||||
for range reloadTicker.C {
|
||||
newNotifs, err := database.LoadNotifications(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to reload notifications: %v\n", err)
|
||||
continue
|
||||
}
|
||||
notifSender.UpdateNotifications(newNotifs)
|
||||
fmt.Println("Reloaded notification configurations")
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean up old entries hourly
|
||||
go func() {
|
||||
deletionTicker := time.NewTicker(time.Hour)
|
||||
defer deletionTicker.Stop()
|
||||
|
||||
for range deletionTicker.C {
|
||||
if err := database.DeleteOldEntries(db); err != nil {
|
||||
fmt.Printf("Error deleting old entries: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for test notifications every 10 seconds
|
||||
go func() {
|
||||
testNotifTicker := time.NewTicker(10 * time.Second)
|
||||
defer testNotifTicker.Stop()
|
||||
|
||||
for range testNotifTicker.C {
|
||||
notifs := notifSender.GetNotifications()
|
||||
database.CheckAndSendTestNotifications(db, notifs, notifSender.SendSpecificNotification)
|
||||
}
|
||||
}()
|
||||
|
||||
// HTTP clients
|
||||
appClient := &http.Client{
|
||||
Timeout: 4 * time.Second,
|
||||
}
|
||||
|
||||
serverClient := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Server monitoring every 5 seconds
|
||||
go func() {
|
||||
serverTicker := time.NewTicker(5 * time.Second)
|
||||
defer serverTicker.Stop()
|
||||
|
||||
for range serverTicker.C {
|
||||
servers, err := database.GetServers(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting servers: %v\n", err)
|
||||
continue
|
||||
}
|
||||
server.MonitorServers(db, serverClient, servers, notifSender)
|
||||
}
|
||||
}()
|
||||
|
||||
// Application monitoring every 10 seconds
|
||||
appTicker := time.NewTicker(time.Second)
|
||||
defer appTicker.Stop()
|
||||
|
||||
for now := range appTicker.C {
|
||||
if now.Second()%10 != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
apps, err := database.GetApplications(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting applications: %v\n", err)
|
||||
continue
|
||||
}
|
||||
app.MonitorApplications(db, appClient, apps, notifSender)
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,22 +1,22 @@
|
||||
module agent
|
||||
module github.com/corecontrol/agent
|
||||
|
||||
go 1.24.1
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v4 v4.18.3
|
||||
github.com/jackc/pgx/v4 v4.18.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgconn v1.14.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
)
|
||||
|
||||
+29
-10
@@ -25,8 +25,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
|
||||
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
@@ -42,8 +42,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
@@ -57,11 +57,12 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
|
||||
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -98,13 +99,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -126,17 +132,22 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -149,15 +160,21 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -165,7 +182,9 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
)
|
||||
|
||||
// MonitorApplications checks and updates the status of all applications
|
||||
func MonitorApplications(db *sql.DB, client *http.Client, apps []models.Application, notifSender *notifications.NotificationSender) {
|
||||
var notificationTemplate string
|
||||
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||
if err != nil || notificationTemplate == "" {
|
||||
notificationTemplate = "The application !name (!url) went !status!"
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
|
||||
fmt.Printf("%s Checking...\n", logPrefix)
|
||||
|
||||
// Determine which URL to use for monitoring
|
||||
checkURL := app.PublicURL
|
||||
if app.UptimeCheckURL != "" {
|
||||
checkURL = app.UptimeCheckURL
|
||||
fmt.Printf("%s Using custom uptime check URL: %s\n", logPrefix, checkURL)
|
||||
}
|
||||
|
||||
parsedURL, parseErr := url.Parse(checkURL)
|
||||
if parseErr != nil {
|
||||
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
|
||||
continue
|
||||
}
|
||||
|
||||
hostIsIP := isIPAddress(parsedURL.Hostname())
|
||||
var isOnline bool
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", checkURL, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode)
|
||||
} else {
|
||||
fmt.Printf("%s Connection error: %v\n", logPrefix, err)
|
||||
|
||||
if hostIsIP {
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
var certErr x509.HostnameError
|
||||
var unknownAuthErr x509.UnknownAuthorityError
|
||||
if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) {
|
||||
fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix)
|
||||
isOnline = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isOnline != app.Online {
|
||||
status := "offline"
|
||||
if isOnline {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(notificationTemplate, "!name", app.Name)
|
||||
message = strings.ReplaceAll(message, "!url", app.PublicURL)
|
||||
message = strings.ReplaceAll(message, "!status", status)
|
||||
|
||||
notifSender.SendNotifications(message)
|
||||
}
|
||||
|
||||
// Update application status in database
|
||||
updateApplicationStatus(db, app.ID, isOnline)
|
||||
|
||||
// Add entry to uptime history
|
||||
addUptimeHistoryEntry(db, app.ID, isOnline)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update application status
|
||||
func updateApplicationStatus(db *sql.DB, appID int, online bool) {
|
||||
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer dbCancel()
|
||||
|
||||
_, err := db.ExecContext(dbCtx,
|
||||
`UPDATE application SET online = $1 WHERE id = $2`,
|
||||
online, appID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("DB update failed for app ID %d: %v\n", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add uptime history entry
|
||||
func addUptimeHistoryEntry(db *sql.DB, appID int, online bool) {
|
||||
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer dbCancel()
|
||||
|
||||
_, err := db.ExecContext(dbCtx,
|
||||
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
||||
appID, online,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("History insert failed for app ID %d: %v\n", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a host is an IP address
|
||||
func isIPAddress(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// InitDB initializes the database connection
|
||||
func InitDB() (*sql.DB, error) {
|
||||
// Load environment variables
|
||||
if err := godotenv.Load(); err != nil {
|
||||
fmt.Println("No env vars found")
|
||||
}
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
return nil, fmt.Errorf("DATABASE_URL not set")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", dbURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database connection failed: %v", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// GetApplications fetches all applications with public URLs
|
||||
func GetApplications(db *sql.DB) ([]models.Application, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, name, "publicURL", online, "uptimeCheckURL" FROM application WHERE "publicURL" IS NOT NULL`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching applications: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []models.Application
|
||||
for rows.Next() {
|
||||
var app models.Application
|
||||
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online, &app.UptimeCheckURL); err != nil {
|
||||
fmt.Printf("Error scanning row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// GetServers fetches all servers with monitoring enabled
|
||||
func GetServers(db *sql.DB) ([]models.Server, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
|
||||
FROM server WHERE monitoring = true`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching servers: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var servers []models.Server
|
||||
for rows.Next() {
|
||||
var server models.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, nil
|
||||
}
|
||||
|
||||
// LoadNotifications loads all enabled notifications
|
||||
func LoadNotifications(db *sql.DB) ([]models.Notification, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
|
||||
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken",
|
||||
"pushoverUrl", "pushoverToken", "pushoverUser"
|
||||
FROM notification
|
||||
WHERE enabled = true`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var configs []models.Notification
|
||||
for rows.Next() {
|
||||
var n models.Notification
|
||||
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.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
|
||||
&n.PushoverUrl, &n.PushoverToken, &n.PushoverUser,
|
||||
); err != nil {
|
||||
fmt.Printf("Error scanning notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
configs = append(configs, n)
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// DeleteOldEntries removes entries older than 30 days
|
||||
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'`,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// UpdateServerStatus updates a server's status and metrics
|
||||
func UpdateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) error {
|
||||
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, "uptime" = $5
|
||||
WHERE id = $6`,
|
||||
online, cpuUsage, ramUsage, diskUsage, uptime, serverID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckAndSendTestNotifications checks for and processes test notifications
|
||||
func CheckAndSendTestNotifications(db *sql.DB, notifications []models.Notification, sendFunc func(models.Notification, string)) {
|
||||
// Query for test notifications
|
||||
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching test notifications: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process each test notification
|
||||
var testIds []int
|
||||
for rows.Next() {
|
||||
var id, notificationId int
|
||||
if err := rows.Scan(&id, ¬ificationId); err != nil {
|
||||
fmt.Printf("Error scanning test notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to list of IDs to delete
|
||||
testIds = append(testIds, id)
|
||||
|
||||
// Find the notification configuration
|
||||
for _, n := range notifications {
|
||||
if n.ID == notificationId {
|
||||
// Send test notification
|
||||
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
|
||||
sendFunc(n, "Test notification from CoreControl")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete processed test notifications
|
||||
if len(testIds) > 0 {
|
||||
for _, id := range testIds {
|
||||
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
ID int
|
||||
Name string
|
||||
PublicURL string
|
||||
Online bool
|
||||
UptimeCheckURL string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ID int
|
||||
Name string
|
||||
Monitoring bool
|
||||
MonitoringURL sql.NullString
|
||||
Online bool
|
||||
CpuUsage sql.NullFloat64
|
||||
RamUsage sql.NullFloat64
|
||||
DiskUsage sql.NullFloat64
|
||||
Uptime sql.NullString
|
||||
}
|
||||
|
||||
type CPUResponse struct {
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
type MemoryResponse struct {
|
||||
Active int64 `json:"active"`
|
||||
Available int64 `json:"available"`
|
||||
Buffers int64 `json:"buffers"`
|
||||
Cached int64 `json:"cached"`
|
||||
Free int64 `json:"free"`
|
||||
Inactive int64 `json:"inactive"`
|
||||
Percent float64 `json:"percent"`
|
||||
Shared int64 `json:"shared"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type FSResponse []struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
MntPoint string `json:"mnt_point"`
|
||||
Percent float64 `json:"percent"`
|
||||
}
|
||||
|
||||
type UptimeResponse struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int
|
||||
Enabled bool
|
||||
Type string
|
||||
SMTPHost sql.NullString
|
||||
SMTPPort sql.NullInt64
|
||||
SMTPFrom sql.NullString
|
||||
SMTPUser sql.NullString
|
||||
SMTPPass sql.NullString
|
||||
SMTPSecure sql.NullBool
|
||||
SMTPTo sql.NullString
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
type NotificationSender struct {
|
||||
notifications []models.Notification
|
||||
notifMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewNotificationSender creates a new notification sender
|
||||
func NewNotificationSender() *NotificationSender {
|
||||
return &NotificationSender{
|
||||
notifications: []models.Notification{},
|
||||
notifMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNotifications updates the stored notifications
|
||||
func (ns *NotificationSender) UpdateNotifications(notifs []models.Notification) {
|
||||
ns.notifMutex.Lock()
|
||||
defer ns.notifMutex.Unlock()
|
||||
|
||||
copyDst := make([]models.Notification, len(notifs))
|
||||
copy(copyDst, notifs)
|
||||
ns.notifications = copyDst
|
||||
}
|
||||
|
||||
// GetNotifications returns a safe copy of current notifications
|
||||
func (ns *NotificationSender) GetNotifications() []models.Notification {
|
||||
ns.notifMutex.RLock()
|
||||
defer ns.notifMutex.RUnlock()
|
||||
|
||||
copyDst := make([]models.Notification, len(ns.notifications))
|
||||
copy(copyDst, ns.notifications)
|
||||
return copyDst
|
||||
}
|
||||
|
||||
// SendNotifications sends a message to all configured notifications
|
||||
func (ns *NotificationSender) SendNotifications(message string) {
|
||||
notifs := ns.GetNotifications()
|
||||
|
||||
for _, n := range notifs {
|
||||
ns.SendSpecificNotification(n, message)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSpecificNotification sends a message to a specific notification
|
||||
func (ns *NotificationSender) SendSpecificNotification(n models.Notification, message string) {
|
||||
fmt.Println("Sending specific notification..." + n.Type)
|
||||
switch n.Type {
|
||||
case "smtp":
|
||||
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||
ns.sendEmail(n, message)
|
||||
}
|
||||
case "telegram":
|
||||
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||
ns.sendTelegram(n, message)
|
||||
}
|
||||
case "discord":
|
||||
if n.DiscordWebhook.Valid {
|
||||
ns.sendDiscord(n, message)
|
||||
}
|
||||
case "gotify":
|
||||
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
|
||||
ns.sendGotify(n, message)
|
||||
}
|
||||
case "ntfy":
|
||||
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
|
||||
ns.sendNtfy(n, message)
|
||||
}
|
||||
case "pushover":
|
||||
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
|
||||
ns.sendPushover(n, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a host is an IP address
|
||||
func (ns *NotificationSender) isIPAddress(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// Individual notification methods
|
||||
func (ns *NotificationSender) sendEmail(n models.Notification, body string) {
|
||||
// Initialize SMTP dialer with host, port, user, pass
|
||||
d := gomail.NewDialer(
|
||||
n.SMTPHost.String,
|
||||
int(n.SMTPPort.Int64),
|
||||
n.SMTPUser.String,
|
||||
n.SMTPPass.String,
|
||||
)
|
||||
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
|
||||
d.SSL = true
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", n.SMTPFrom.String)
|
||||
m.SetHeader("To", n.SMTPTo.String)
|
||||
m.SetHeader("Subject", "Uptime Notification")
|
||||
m.SetBody("text/plain", body)
|
||||
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
fmt.Printf("Email send failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendTelegram(n models.Notification, message string) {
|
||||
apiUrl := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
|
||||
n.TelegramToken.String,
|
||||
n.TelegramChatID.String,
|
||||
message,
|
||||
)
|
||||
resp, err := http.Get(apiUrl)
|
||||
if err != nil {
|
||||
fmt.Printf("Telegram send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendDiscord(n models.Notification, message string) {
|
||||
payload := fmt.Sprintf(`{"content": "%s"}`, message)
|
||||
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
fmt.Printf("Discord request creation failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Discord send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendGotify(n models.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 (ns *NotificationSender) sendNtfy(n models.Notification, message string) {
|
||||
fmt.Println("Sending Ntfy notification...")
|
||||
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
|
||||
|
||||
// Don't append a topic to the URL - the URL itself should have the correct endpoint
|
||||
requestURL := baseURL
|
||||
|
||||
// Send message directly as request body instead of JSON
|
||||
req, err := http.NewRequest("POST", requestURL, strings.NewReader(message))
|
||||
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)
|
||||
}
|
||||
// Use text/plain instead of application/json
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
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 (ns *NotificationSender) sendPushover(n models.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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
)
|
||||
|
||||
// MonitorServers checks and updates the status of all servers
|
||||
func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, notifSender *notifications.NotificationSender) {
|
||||
var notificationTemplate string
|
||||
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||
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
|
||||
var uptimeStr string
|
||||
|
||||
// Get CPU usage
|
||||
online, cpuUsage = fetchCPUUsage(client, baseURL, logPrefix)
|
||||
if !online {
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get uptime if server is online
|
||||
uptimeStr = fetchUptime(client, baseURL, logPrefix)
|
||||
|
||||
// Get Memory usage
|
||||
memOnline, memUsage := fetchMemoryUsage(client, baseURL, logPrefix)
|
||||
if !memOnline {
|
||||
online = false
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
ramUsage = memUsage
|
||||
|
||||
// Get Disk usage
|
||||
diskOnline, diskUsageVal := fetchDiskUsage(client, baseURL, logPrefix)
|
||||
if !diskOnline {
|
||||
online = false
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
diskUsage = diskUsageVal
|
||||
|
||||
// Check if status changed and send notification if needed
|
||||
if online != server.Online {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
|
||||
// Update server status with metrics
|
||||
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
|
||||
// Add entry to server history
|
||||
addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
|
||||
|
||||
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n",
|
||||
logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch CPU usage
|
||||
func fetchCPUUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer cpuResp.Body.Close()
|
||||
|
||||
if cpuResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var cpuData models.CPUResponse
|
||||
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
|
||||
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, cpuData.Total
|
||||
}
|
||||
|
||||
// Helper function to fetch memory usage
|
||||
func fetchMemoryUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer memResp.Body.Close()
|
||||
|
||||
if memResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var memData models.MemoryResponse
|
||||
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
|
||||
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, memData.Percent
|
||||
}
|
||||
|
||||
// Helper function to fetch disk usage
|
||||
func fetchDiskUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer fsResp.Body.Close()
|
||||
|
||||
if fsResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var fsData models.FSResponse
|
||||
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
|
||||
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if len(fsData) > 0 {
|
||||
return true, fsData[0].Percent
|
||||
}
|
||||
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// Helper function to fetch uptime
|
||||
func fetchUptime(client *http.Client, baseURL, logPrefix string) string {
|
||||
uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL))
|
||||
if err != nil || uptimeResp.StatusCode != http.StatusOK {
|
||||
if err != nil {
|
||||
fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err)
|
||||
} else {
|
||||
fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode)
|
||||
uptimeResp.Body.Close()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
defer uptimeResp.Body.Close()
|
||||
|
||||
// Read the response body as a string first
|
||||
uptimeBytes, err := io.ReadAll(uptimeResp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
uptimeStr := strings.Trim(string(uptimeBytes), "\"")
|
||||
|
||||
// Try to parse as JSON object first, then fallback to direct string if that fails
|
||||
var uptimeData models.UptimeResponse
|
||||
if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" {
|
||||
uptimeStr = formatUptime(uptimeData.Value)
|
||||
} else {
|
||||
// Use the string directly
|
||||
uptimeStr = formatUptime(uptimeStr)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr)
|
||||
return uptimeStr
|
||||
}
|
||||
|
||||
// Helper function to send notification about status change
|
||||
func sendStatusChangeNotification(server models.Server, online bool, template string, notifSender *notifications.NotificationSender) {
|
||||
status := "offline"
|
||||
if online {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(template, "!name", server.Name)
|
||||
message = strings.ReplaceAll(message, "!status", status)
|
||||
|
||||
notifSender.SendNotifications(message)
|
||||
}
|
||||
|
||||
// Helper function to update server status
|
||||
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) {
|
||||
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, "uptime" = $5
|
||||
WHERE id = $6`,
|
||||
online, cpuUsage, ramUsage, diskUsage, uptime, serverID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add server history entry
|
||||
func addServerHistoryEntry(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,
|
||||
`INSERT INTO server_history(
|
||||
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt"
|
||||
) VALUES ($1, $2, $3, $4, $5, now())`,
|
||||
serverID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to insert server history (ID: %d): %v\n", serverID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// FormatUptime formats the uptime string to a standard format
|
||||
func formatUptime(uptimeStr string) string {
|
||||
// Example input: "3 days, 3:52:36"
|
||||
// Target output: "28.6 13:52"
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Parse the uptime components
|
||||
parts := strings.Split(uptimeStr, ", ")
|
||||
|
||||
var days int
|
||||
var timeStr string
|
||||
|
||||
if len(parts) == 2 {
|
||||
// Has days part and time part
|
||||
_, err := fmt.Sscanf(parts[0], "%d days", &days)
|
||||
if err != nil {
|
||||
// Try singular "day"
|
||||
_, err = fmt.Sscanf(parts[0], "%d day", &days)
|
||||
if err != nil {
|
||||
return uptimeStr // Return original if parsing fails
|
||||
}
|
||||
}
|
||||
timeStr = parts[1]
|
||||
} else if len(parts) == 1 {
|
||||
// Only has time part (less than a day)
|
||||
days = 0
|
||||
timeStr = parts[0]
|
||||
} else {
|
||||
return uptimeStr // Return original if format is unexpected
|
||||
}
|
||||
|
||||
// Parse the time component (hours:minutes:seconds)
|
||||
var hours, minutes, seconds int
|
||||
_, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds)
|
||||
if err != nil {
|
||||
return uptimeStr // Return original if parsing fails
|
||||
}
|
||||
|
||||
// Calculate the total duration
|
||||
duration := time.Duration(days)*24*time.Hour +
|
||||
time.Duration(hours)*time.Hour +
|
||||
time.Duration(minutes)*time.Minute +
|
||||
time.Duration(seconds)*time.Second
|
||||
|
||||
// Calculate the start time by subtracting the duration from now
|
||||
startTime := now.Add(-duration)
|
||||
|
||||
// Format the result in the required format (day.month hour:minute)
|
||||
return startTime.Format("2.1 15:04")
|
||||
}
|
||||
-805
@@ -1,805 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
ID int
|
||||
Name string
|
||||
PublicURL string
|
||||
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 {
|
||||
Active int64 `json:"active"`
|
||||
Available int64 `json:"available"`
|
||||
Buffers int64 `json:"buffers"`
|
||||
Cached int64 `json:"cached"`
|
||||
Free int64 `json:"free"`
|
||||
Inactive int64 `json:"inactive"`
|
||||
Percent float64 `json:"percent"`
|
||||
Shared int64 `json:"shared"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type FSResponse []struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
MntPoint string `json:"mnt_point"`
|
||||
Percent float64 `json:"percent"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int
|
||||
Enabled bool
|
||||
Type string
|
||||
SMTPHost sql.NullString
|
||||
SMTPPort sql.NullInt64
|
||||
SMTPFrom sql.NullString
|
||||
SMTPUser sql.NullString
|
||||
SMTPPass sql.NullString
|
||||
SMTPSecure sql.NullBool
|
||||
SMTPTo sql.NullString
|
||||
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 (
|
||||
notifications []Notification
|
||||
notifMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
fmt.Println("No env vars found")
|
||||
}
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
panic("DATABASE_URL not set")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", dbURL)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Database connection failed: %v\n", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// initial load
|
||||
notifs, err := loadNotifications(db)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load notifications: %v", err))
|
||||
}
|
||||
notifMutex.Lock()
|
||||
notifications = notifMutexCopy(notifs)
|
||||
notifMutex.Unlock()
|
||||
|
||||
// reload notification configs every minute
|
||||
go func() {
|
||||
reloadTicker := time.NewTicker(time.Minute)
|
||||
defer reloadTicker.Stop()
|
||||
|
||||
for range reloadTicker.C {
|
||||
newNotifs, err := loadNotifications(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to reload notifications: %v\n", err)
|
||||
continue
|
||||
}
|
||||
notifMutex.Lock()
|
||||
notifications = notifMutexCopy(newNotifs)
|
||||
notifMutex.Unlock()
|
||||
fmt.Println("Reloaded notification configurations")
|
||||
}
|
||||
}()
|
||||
|
||||
// clean up old entries hourly
|
||||
go func() {
|
||||
deletionTicker := time.NewTicker(time.Hour)
|
||||
defer deletionTicker.Stop()
|
||||
|
||||
for range deletionTicker.C {
|
||||
if err := deleteOldEntries(db); err != nil {
|
||||
fmt.Printf("Error deleting old entries: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for test notifications every 10 seconds
|
||||
go func() {
|
||||
testNotifTicker := time.NewTicker(10 * time.Second)
|
||||
defer testNotifTicker.Stop()
|
||||
|
||||
for range testNotifTicker.C {
|
||||
checkAndSendTestNotifications(db)
|
||||
}
|
||||
}()
|
||||
|
||||
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, appClient, apps)
|
||||
}
|
||||
}
|
||||
|
||||
// helper to safely copy slice
|
||||
func notifMutexCopy(src []Notification) []Notification {
|
||||
copyDst := make([]Notification, len(src))
|
||||
copy(copyDst, src)
|
||||
return copyDst
|
||||
}
|
||||
|
||||
func isIPAddress(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
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", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken"
|
||||
FROM notification
|
||||
WHERE enabled = true`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var configs []Notification
|
||||
for rows.Next() {
|
||||
var n Notification
|
||||
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.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
|
||||
); err != nil {
|
||||
fmt.Printf("Error scanning notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
configs = append(configs, n)
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
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'`,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func getApplications(db *sql.DB) []Application {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching applications: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []Application
|
||||
for rows.Next() {
|
||||
var app Application
|
||||
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil {
|
||||
fmt.Printf("Error scanning row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
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_application FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||
if err != nil || notificationTemplate == "" {
|
||||
notificationTemplate = "The application !name (!url) went !status!"
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
|
||||
fmt.Printf("%s Checking...\n", logPrefix)
|
||||
|
||||
parsedURL, parseErr := url.Parse(app.PublicURL)
|
||||
if parseErr != nil {
|
||||
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
|
||||
continue
|
||||
}
|
||||
|
||||
hostIsIP := isIPAddress(parsedURL.Hostname())
|
||||
var isOnline bool
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", app.PublicURL, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode)
|
||||
} else {
|
||||
fmt.Printf("%s Connection error: %v\n", logPrefix, err)
|
||||
|
||||
if hostIsIP {
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
var certErr x509.HostnameError
|
||||
var unknownAuthErr x509.UnknownAuthorityError
|
||||
if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) {
|
||||
fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix)
|
||||
isOnline = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isOnline != app.Online {
|
||||
status := "offline"
|
||||
if isOnline {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(notificationTemplate, "!name", app.Name)
|
||||
message = strings.ReplaceAll(message, "!url", app.PublicURL)
|
||||
message = strings.ReplaceAll(message, "!status", status)
|
||||
|
||||
sendNotifications(message)
|
||||
}
|
||||
|
||||
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, err = db.ExecContext(dbCtx,
|
||||
`UPDATE application SET online = $1 WHERE id = $2`,
|
||||
isOnline, app.ID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("%s DB update failed: %v\n", logPrefix, err)
|
||||
}
|
||||
dbCancel()
|
||||
|
||||
dbCtx2, dbCancel2 := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, err = db.ExecContext(dbCtx2,
|
||||
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
||||
app.ID, isOnline,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("%s History insert failed: %v\n", logPrefix, err)
|
||||
}
|
||||
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(¬ificationTemplate)
|
||||
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 {
|
||||
// Calculate actual RAM usage excluding swap, cache, and buffers
|
||||
// Formula: (total - free - cached - buffers) / total * 100
|
||||
// This is the most accurate representation of actual used RAM
|
||||
actualUsedRam := memData.Total - memData.Free - memData.Cached - memData.Buffers
|
||||
if actualUsedRam < 0 {
|
||||
actualUsedRam = 0 // Safeguard against negative values
|
||||
}
|
||||
|
||||
if memData.Total > 0 {
|
||||
ramUsage = float64(actualUsedRam) / float64(memData.Total) * 100
|
||||
fmt.Printf("%s Calculated RAM usage: %.2f%% (Used: %d MB, Total: %d MB)\n",
|
||||
logPrefix, ramUsage, actualUsedRam/1024/1024, memData.Total/1024/1024)
|
||||
} else {
|
||||
// Fallback to the provided percentage if calculation fails
|
||||
ramUsage = memData.Percent
|
||||
fmt.Printf("%s Using provided memory percentage because total is zero\n", logPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
notifMutex.RUnlock()
|
||||
|
||||
for _, n := range notifs {
|
||||
switch n.Type {
|
||||
case "email":
|
||||
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||
sendEmail(n, message)
|
||||
}
|
||||
case "telegram":
|
||||
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||
sendTelegram(n, message)
|
||||
}
|
||||
case "discord":
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendEmail(n Notification, body string) {
|
||||
// Initialize SMTP dialer with host, port, user, pass
|
||||
d := gomail.NewDialer(
|
||||
n.SMTPHost.String,
|
||||
int(n.SMTPPort.Int64),
|
||||
n.SMTPUser.String,
|
||||
n.SMTPPass.String,
|
||||
)
|
||||
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
|
||||
d.SSL = true
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", n.SMTPFrom.String)
|
||||
m.SetHeader("To", n.SMTPTo.String)
|
||||
m.SetHeader("Subject", "Uptime Notification")
|
||||
m.SetBody("text/plain", body)
|
||||
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
fmt.Printf("Email send failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendTelegram(n Notification, message string) {
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
|
||||
n.TelegramToken.String,
|
||||
n.TelegramChatID.String,
|
||||
message,
|
||||
)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("Telegram send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func sendDiscord(n Notification, message string) {
|
||||
payload := fmt.Sprintf(`{"content": "%s"}`, message)
|
||||
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
fmt.Printf("Discord request creation failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Discord send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndSendTestNotifications(db *sql.DB) {
|
||||
// Query for test notifications
|
||||
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching test notifications: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process each test notification
|
||||
var testIds []int
|
||||
for rows.Next() {
|
||||
var id, notificationId int
|
||||
if err := rows.Scan(&id, ¬ificationId); err != nil {
|
||||
fmt.Printf("Error scanning test notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to list of IDs to delete
|
||||
testIds = append(testIds, id)
|
||||
|
||||
// Find the notification configuration
|
||||
notifMutex.RLock()
|
||||
for _, n := range notifications {
|
||||
if n.ID == notificationId {
|
||||
// Send test notification
|
||||
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
|
||||
sendSpecificNotification(n, "Test notification from CoreControl")
|
||||
}
|
||||
}
|
||||
notifMutex.RUnlock()
|
||||
}
|
||||
|
||||
// Delete processed test notifications
|
||||
if len(testIds) > 0 {
|
||||
for _, id := range testIds {
|
||||
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendSpecificNotification(n Notification, message string) {
|
||||
switch n.Type {
|
||||
case "email":
|
||||
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||
sendEmail(n, message)
|
||||
}
|
||||
case "telegram":
|
||||
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||
sendTelegram(n, message)
|
||||
}
|
||||
case "discord":
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@ interface AddRequest {
|
||||
icon: string;
|
||||
publicURL: string;
|
||||
localURL: string;
|
||||
uptimecheckUrl: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: AddRequest = await request.json();
|
||||
const { serverId, name, description, icon, publicURL, localURL } = body;
|
||||
const { serverId, name, description, icon, publicURL, localURL, uptimecheckUrl } = body;
|
||||
|
||||
const application = await prisma.application.create({
|
||||
data: {
|
||||
@@ -22,7 +23,8 @@ export async function POST(request: NextRequest) {
|
||||
description,
|
||||
icon,
|
||||
publicURL,
|
||||
localURL
|
||||
localURL,
|
||||
uptimecheckUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ interface EditRequest {
|
||||
icon: string;
|
||||
publicURL: string;
|
||||
localURL: string;
|
||||
uptimecheckUrl: string;
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body: EditRequest = await request.json();
|
||||
const { id, name, description, serverId, icon, publicURL, localURL } = body;
|
||||
const { id, name, description, serverId, icon, publicURL, localURL, uptimecheckUrl } = body;
|
||||
|
||||
const existingApp = await prisma.application.findUnique({ where: { id } });
|
||||
if (!existingApp) {
|
||||
@@ -29,7 +30,8 @@ export async function PUT(request: NextRequest) {
|
||||
description,
|
||||
icon,
|
||||
publicURL,
|
||||
localURL
|
||||
localURL,
|
||||
uptimecheckUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
applications: applicationsWithServers,
|
||||
servers: servers_all,
|
||||
maxPage
|
||||
maxPage,
|
||||
totalItems: totalCount
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
@@ -23,7 +23,23 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const searchResults = fuse.search(searchterm);
|
||||
|
||||
const results = searchResults.map(({ item }) => item);
|
||||
const searchedApps = searchResults.map(({ item }) => item);
|
||||
|
||||
// Get server IDs from the search results
|
||||
const serverIds = searchedApps
|
||||
.map(app => app.serverId)
|
||||
.filter((id): id is number => id !== null);
|
||||
|
||||
// Fetch server data for these applications
|
||||
const servers = await prisma.server.findMany({
|
||||
where: { id: { in: serverIds } }
|
||||
});
|
||||
|
||||
// Add server name to each application
|
||||
const results = searchedApps.map(app => ({
|
||||
...app,
|
||||
server: servers.find(s => s.id === app.serverId)?.name || "No server"
|
||||
}));
|
||||
|
||||
return NextResponse.json({ results });
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
|
||||
interface RequestBody {
|
||||
timespan?: number;
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,8 +101,7 @@ const getIntervalKey = (date: Date, timespan: number) => {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { timespan = 1, page = 1 }: RequestBody = await request.json();
|
||||
const itemsPerPage = 5;
|
||||
const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json();
|
||||
const skip = (page - 1) * itemsPerPage;
|
||||
|
||||
// Get paginated and sorted applications
|
||||
|
||||
@@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface AddRequest {
|
||||
type: string;
|
||||
name: string;
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
smtpSecure?: boolean;
|
||||
@@ -25,11 +26,12 @@ interface AddRequest {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: AddRequest = await request.json();
|
||||
const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser } = body;
|
||||
const { type, name, 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: {
|
||||
type: type,
|
||||
name: name,
|
||||
smtpHost: smtpHost,
|
||||
smtpPort: smtpPort,
|
||||
smtpFrom: smtpFrom,
|
||||
|
||||
@@ -233,8 +233,9 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Only calculate maxPage when not requesting a specific server
|
||||
let maxPage = 1;
|
||||
let totalHosts = 0;
|
||||
if (!serverId) {
|
||||
const totalHosts = await prisma.server.count({
|
||||
totalHosts = await prisma.server.count({
|
||||
where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
|
||||
});
|
||||
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
|
||||
@@ -242,7 +243,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
servers: hostsWithVms,
|
||||
maxPage
|
||||
maxPage,
|
||||
totalItems: totalHosts
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function GET() {
|
||||
cpuUsage: true,
|
||||
ramUsage: true,
|
||||
diskUsage: true,
|
||||
uptime: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,12 +21,14 @@ export async function GET() {
|
||||
cpuUsage: string | null;
|
||||
ramUsage: string | null;
|
||||
diskUsage: string | null;
|
||||
uptime: 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
|
||||
diskUsage: server.diskUsage ? parseInt(server.diskUsage) : 0,
|
||||
uptime: server.uptime || ""
|
||||
}));
|
||||
|
||||
return NextResponse.json(monitoringData)
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
List,
|
||||
Pencil,
|
||||
Zap,
|
||||
ViewIcon,
|
||||
Grid3X3,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
@@ -65,7 +68,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -76,6 +79,12 @@ import {
|
||||
import { StatusIndicator } from "@/components/status-indicator";
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Application {
|
||||
id: number;
|
||||
@@ -87,6 +96,7 @@ interface Application {
|
||||
server?: string;
|
||||
online: boolean;
|
||||
serverId: number;
|
||||
uptimecheckUrl?: string;
|
||||
}
|
||||
|
||||
interface Server {
|
||||
@@ -98,6 +108,7 @@ interface ApplicationsResponse {
|
||||
applications: Application[];
|
||||
servers: Server[];
|
||||
maxPage: number;
|
||||
totalItems?: number;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
@@ -107,6 +118,8 @@ export default function Dashboard() {
|
||||
const [publicURL, setPublicURL] = useState<string>("");
|
||||
const [localURL, setLocalURL] = useState<string>("");
|
||||
const [serverId, setServerId] = useState<number | null>(null);
|
||||
const [customUptimeCheck, setCustomUptimeCheck] = useState<boolean>(false);
|
||||
const [uptimecheckUrl, setUptimecheckUrl] = useState<string>("");
|
||||
|
||||
const [editName, setEditName] = useState<string>("");
|
||||
const [editDescription, setEditDescription] = useState<string>("");
|
||||
@@ -115,6 +128,8 @@ export default function Dashboard() {
|
||||
const [editLocalURL, setEditLocalURL] = useState<string>("");
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [editServerId, setEditServerId] = useState<number | null>(null);
|
||||
const [editCustomUptimeCheck, setEditCustomUptimeCheck] = useState<boolean>(false);
|
||||
const [editUptimecheckUrl, setEditUptimecheckUrl] = useState<string>("");
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [maxPage, setMaxPage] = useState<number>(1);
|
||||
@@ -126,21 +141,72 @@ export default function Dashboard() {
|
||||
const [isSearching, setIsSearching] = useState<boolean>(false);
|
||||
|
||||
const savedLayout = Cookies.get("layoutPreference-app");
|
||||
const savedItemsPerPage = Cookies.get("itemsPerPage-app");
|
||||
const initialIsGridLayout = savedLayout === "grid";
|
||||
const initialItemsPerPage = initialIsGridLayout ? 15 : 5;
|
||||
const initialIsCompactLayout = savedLayout === "compact";
|
||||
const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5);
|
||||
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
|
||||
|
||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
|
||||
const [isCompactLayout, setIsCompactLayout] = useState<boolean>(initialIsCompactLayout);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const toggleLayout = () => {
|
||||
const newLayout = !isGridLayout;
|
||||
setIsGridLayout(newLayout);
|
||||
Cookies.set("layoutPreference-app", newLayout ? "grid" : "standard", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
setItemsPerPage(newLayout ? 15 : 5);
|
||||
const toggleLayout = (layout: string) => {
|
||||
if (layout === "standard") {
|
||||
setIsGridLayout(false);
|
||||
setIsCompactLayout(false);
|
||||
Cookies.set("layoutPreference-app", "standard", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (layout === "grid") {
|
||||
setIsGridLayout(true);
|
||||
setIsCompactLayout(false);
|
||||
Cookies.set("layoutPreference-app", "grid", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (layout === "compact") {
|
||||
setIsGridLayout(false);
|
||||
setIsCompactLayout(true);
|
||||
Cookies.set("layoutPreference-app", "compact", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (value: string) => {
|
||||
// Clear any existing timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timer
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Ensure the value is within the valid range
|
||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||
toast.error("Please enter a number between 1 and 100");
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1); // Reset to first page when changing items per page
|
||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}, 300); // 300ms delay
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
@@ -152,6 +218,7 @@ export default function Dashboard() {
|
||||
publicURL,
|
||||
localURL,
|
||||
serverId,
|
||||
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
|
||||
});
|
||||
getApplications();
|
||||
toast.success("Application added successfully");
|
||||
@@ -171,6 +238,9 @@ export default function Dashboard() {
|
||||
setApplications(response.data.applications);
|
||||
setServers(response.data.servers);
|
||||
setMaxPage(response.data.maxPage);
|
||||
if (response.data.totalItems !== undefined) {
|
||||
setTotalItems(response.data.totalItems);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
@@ -178,6 +248,11 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate current range of items being displayed
|
||||
const [totalItems, setTotalItems] = useState<number>(0);
|
||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
useEffect(() => {
|
||||
getApplications();
|
||||
}, [currentPage, itemsPerPage]);
|
||||
@@ -205,6 +280,14 @@ export default function Dashboard() {
|
||||
setEditIcon(app.icon || "");
|
||||
setEditLocalURL(app.localURL || "");
|
||||
setEditPublicURL(app.publicURL || "");
|
||||
|
||||
if (app.uptimecheckUrl) {
|
||||
setEditCustomUptimeCheck(true);
|
||||
setEditUptimecheckUrl(app.uptimecheckUrl);
|
||||
} else {
|
||||
setEditCustomUptimeCheck(false);
|
||||
setEditUptimecheckUrl("");
|
||||
}
|
||||
};
|
||||
|
||||
const edit = async () => {
|
||||
@@ -219,6 +302,7 @@ export default function Dashboard() {
|
||||
icon: editIcon,
|
||||
publicURL: editPublicURL,
|
||||
localURL: editLocalURL,
|
||||
uptimecheckUrl: editCustomUptimeCheck ? editUptimecheckUrl : "",
|
||||
});
|
||||
getApplications();
|
||||
setEditId(null);
|
||||
@@ -294,20 +378,115 @@ export default function Dashboard() {
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Your Applications</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleLayout}
|
||||
title={
|
||||
isGridLayout ? "Switch to list view" : "Switch to grid view"
|
||||
}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" title="Change view">
|
||||
{isCompactLayout ? (
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
) : isGridLayout ? (
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
) : (
|
||||
<List className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
|
||||
<List className="h-4 w-4 mr-2" /> List View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
|
||||
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
|
||||
<Grid3X3 className="h-4 w-4 mr-2" /> Compact View
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
onValueChange={handleItemsPerPageChange}
|
||||
onOpenChange={(open) => {
|
||||
if (open && customInputRef.current) {
|
||||
customInputRef.current.value = String(itemsPerPage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGridLayout ? (
|
||||
<List className="h-4 w-4" />
|
||||
) : (
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
<SelectItem value="5">5 items</SelectItem>
|
||||
<SelectItem value="10">10 items</SelectItem>
|
||||
<SelectItem value="15">15 items</SelectItem>
|
||||
<SelectItem value="20">20 items</SelectItem>
|
||||
<SelectItem value="25">25 items</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
ref={customInputRef}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
className="h-8"
|
||||
defaultValue={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
// Don't immediately apply the change while typing
|
||||
// Just validate the input for visual feedback
|
||||
const value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1 || value > 100) {
|
||||
e.target.classList.add("border-red-500");
|
||||
} else {
|
||||
e.target.classList.remove("border-red-500");
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// Apply the change when the input loses focus
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
handleItemsPerPageChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Clear any existing debounce timer to apply immediately
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
const value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
// Apply change immediately on Enter
|
||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1);
|
||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Close the dropdown
|
||||
document.body.click();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{servers.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
You must first add a server.
|
||||
@@ -404,6 +583,36 @@ export default function Dashboard() {
|
||||
onChange={(e) => setLocalURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="custom-uptime-check"
|
||||
checked={customUptimeCheck}
|
||||
onChange={(e) => setCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{customUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={uptimecheckUrl}
|
||||
onChange={(e) => setUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -433,59 +642,291 @@ export default function Dashboard() {
|
||||
{!loading ? (
|
||||
<div
|
||||
className={
|
||||
isGridLayout
|
||||
isCompactLayout
|
||||
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2"
|
||||
: isGridLayout
|
||||
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{applications.map((app) => (
|
||||
<Card
|
||||
key={app.id}
|
||||
className={
|
||||
isGridLayout
|
||||
? "h-full flex flex-col justify-between relative"
|
||||
: "w-full mb-4 relative"
|
||||
}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="absolute top-2 right-2">
|
||||
<StatusIndicator isOnline={app.online} />
|
||||
isCompactLayout ? (
|
||||
<div
|
||||
key={app.id}
|
||||
className="bg-card rounded-md border p-3 flex flex-col items-center justify-between h-[120px] w-full cursor-pointer hover:shadow-md transition-shadow relative"
|
||||
onClick={() => window.open(app.publicURL, "_blank")}
|
||||
title={app.name}
|
||||
>
|
||||
<div className="absolute top-1 right-1">
|
||||
<StatusIndicator isOnline={app.online} showLabel={false} />
|
||||
</div>
|
||||
<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 ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Image</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
{app.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-md">
|
||||
{app.description}
|
||||
{app.description && (
|
||||
<br className="hidden md:block" />
|
||||
)}
|
||||
Server: {app.server || "No server"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
|
||||
{app.icon ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Icon</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<h3 className="text-sm font-medium truncate w-full max-w-[110px]">{app.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
key={app.id}
|
||||
className={
|
||||
isGridLayout
|
||||
? "h-full flex flex-col justify-between relative"
|
||||
: "w-full mb-4 relative"
|
||||
}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="absolute top-2 right-2">
|
||||
<StatusIndicator isOnline={app.online} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
|
||||
<div className={`flex ${isGridLayout ? 'flex-col' : '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 ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Image</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
{app.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-md">
|
||||
{app.description}
|
||||
{app.description && (
|
||||
<br className="hidden md:block" />
|
||||
)}
|
||||
Server: {app.server || "No server"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isGridLayout && (
|
||||
<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
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.publicURL, "_blank")
|
||||
}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Public URL
|
||||
</Button>
|
||||
{app.localURL && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.localURL, "_blank")
|
||||
}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Local URL
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={() => deleteApplication(app.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={() => openEditDialog(app)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Edit Application
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Name</Label>
|
||||
<Input
|
||||
placeholder="e.g. Portainer"
|
||||
value={editName}
|
||||
onChange={(e) =>
|
||||
setEditName(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Server</Label>
|
||||
<Select
|
||||
value={
|
||||
editServerId !== null
|
||||
? String(editServerId)
|
||||
: undefined
|
||||
}
|
||||
onValueChange={(v) =>
|
||||
setEditServerId(Number(v))
|
||||
}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{servers.map((server) => (
|
||||
<SelectItem
|
||||
key={server.id}
|
||||
value={String(server.id)}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Description{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Application description"
|
||||
value={editDescription}
|
||||
onChange={(e) =>
|
||||
setEditDescription(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Icon URL{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="https://example.com/icon.png"
|
||||
value={editIcon}
|
||||
onChange={(e) =>
|
||||
setEditIcon(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={generateEditIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Public URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={editPublicURL}
|
||||
onChange={(e) =>
|
||||
setEditPublicURL(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Local URL{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:3000"
|
||||
value={editLocalURL}
|
||||
onChange={(e) =>
|
||||
setEditLocalURL(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-custom-uptime-check"
|
||||
checked={editCustomUptimeCheck}
|
||||
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{editCustomUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={editUptimecheckUrl}
|
||||
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={edit}
|
||||
disabled={
|
||||
!editName || !editPublicURL || !editServerId
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{isGridLayout && (
|
||||
<CardFooter className="mt-auto">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex flex-col space-y-2 flex-grow">
|
||||
<div className={`grid ${app.localURL ? 'grid-cols-2' : 'grid-cols-1'} gap-2 flex-grow`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.publicURL, "_blank")
|
||||
}
|
||||
onClick={() => window.open(app.publicURL, "_blank")}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Public URL
|
||||
@@ -494,16 +935,14 @@ export default function Dashboard() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.localURL, "_blank")
|
||||
}
|
||||
onClick={() => window.open(app.localURL, "_blank")}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Local URL
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
@@ -627,6 +1066,36 @@ export default function Dashboard() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-custom-uptime-check"
|
||||
checked={editCustomUptimeCheck}
|
||||
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{editCustomUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={editUptimecheckUrl}
|
||||
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -645,10 +1114,10 @@ export default function Dashboard() {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -680,6 +1149,11 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
@@ -64,6 +64,7 @@ import Chart from 'chart.js/auto'
|
||||
import NextLink from "next/link"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { toast } from "sonner"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
interface ServerHistory {
|
||||
labels: string[];
|
||||
@@ -98,11 +99,13 @@ interface Server {
|
||||
diskUsage: number;
|
||||
history?: ServerHistory;
|
||||
port: number;
|
||||
uptime: string;
|
||||
}
|
||||
|
||||
interface GetServersResponse {
|
||||
servers: Server[]
|
||||
maxPage: number
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
interface MonitoringData {
|
||||
@@ -111,6 +114,7 @@ interface MonitoringData {
|
||||
cpuUsage: number
|
||||
ramUsage: number
|
||||
diskUsage: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
@@ -136,6 +140,7 @@ export default function Dashboard() {
|
||||
const [maxPage, setMaxPage] = useState<number>(1)
|
||||
const [servers, setServers] = useState<Server[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [totalItems, setTotalItems] = useState<number>(0)
|
||||
|
||||
const [editId, setEditId] = useState<number | null>(null)
|
||||
const [editHost, setEditHost] = useState<boolean>(false)
|
||||
@@ -161,21 +166,23 @@ export default function Dashboard() {
|
||||
const [monitoringInterval, setMonitoringInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const savedLayout = Cookies.get("layoutPreference-servers");
|
||||
const savedItemsPerPage = Cookies.get("itemsPerPage-servers");
|
||||
const initialIsGridLayout = savedLayout === "grid";
|
||||
const initialItemsPerPage = initialIsGridLayout ? 6 : 4;
|
||||
const defaultItemsPerPage = initialIsGridLayout ? 6 : 4;
|
||||
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
|
||||
|
||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const toggleLayout = () => {
|
||||
const newLayout = !isGridLayout;
|
||||
setIsGridLayout(newLayout);
|
||||
Cookies.set("layoutPreference-servers", newLayout ? "grid" : "standard", {
|
||||
const toggleLayout = (gridLayout: boolean) => {
|
||||
setIsGridLayout(gridLayout);
|
||||
Cookies.set("layoutPreference-servers", gridLayout ? "grid" : "standard", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
setItemsPerPage(newLayout ? 6 : 4); // Update itemsPerPage based on new layout
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
@@ -231,6 +238,7 @@ export default function Dashboard() {
|
||||
setServers(response.data.servers)
|
||||
console.log(response.data.servers)
|
||||
setMaxPage(response.data.maxPage)
|
||||
setTotalItems(response.data.totalItems)
|
||||
setLoading(false)
|
||||
} catch (error: any) {
|
||||
console.log(error.response)
|
||||
@@ -355,8 +363,8 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
const iconCategories = {
|
||||
Infrastructure: ["server", "network", "database", "cloud", "hard-drive", "router", "wifi", "antenna"],
|
||||
Computing: ["cpu", "microchip", "memory-stick", "terminal", "code", "binary", "command"],
|
||||
Infrastructure: ["server", "network", "database", "database-backup", "cloud", "hard-drive", "router", "wifi", "antenna"],
|
||||
Computing: ["cpu", "microchip", "memory-stick", "terminal", "code", "binary", "command", "ethernet-port"],
|
||||
Monitoring: ["activity", "monitor", "gauge", "bar-chart", "line-chart", "pie-chart"],
|
||||
Security: ["shield", "lock", "key", "fingerprint", "scan-face"],
|
||||
Status: ["check-circle", "x-octagon", "alert-triangle", "alarm-check", "life-buoy"],
|
||||
@@ -451,6 +459,61 @@ export default function Dashboard() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handler für benutzerdefinierte Zahleneingaben mit Verzögerung
|
||||
const handleItemsPerPageChange = (value: string) => {
|
||||
// Bestehenden Timer löschen
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Neuen Timer setzen
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Sicherstellen, dass der Wert im gültigen Bereich liegt
|
||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||
toast.error("Bitte eine Zahl zwischen 1 und 100 eingeben");
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1); // Zurück zur ersten Seite
|
||||
Cookies.set("itemsPerPage-servers", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Daten mit neuer Paginierung abrufen
|
||||
getServers();
|
||||
}, 600); // 600ms Verzögerung für bessere Eingabe mehrziffriger Zahlen
|
||||
};
|
||||
|
||||
// Handler für voreingestellte Werte aus dem Dropdown
|
||||
const handlePresetItemsPerPageChange = (value: string) => {
|
||||
// Für voreingestellte Werte sofort anwenden
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Nur Standardwerte hier verarbeiten
|
||||
if ([4, 6, 10, 15, 20, 25].includes(newItemsPerPage)) {
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1); // Zurück zur ersten Seite
|
||||
Cookies.set("itemsPerPage-servers", String(newItemsPerPage), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Daten mit neuer Paginierung abrufen
|
||||
getServers();
|
||||
} else {
|
||||
// Für benutzerdefinierte Werte den verzögerten Handler verwenden
|
||||
handleItemsPerPageChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
@@ -480,17 +543,119 @@ export default function Dashboard() {
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Your Servers</span>
|
||||
<div className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon" onClick={toggleLayout}>
|
||||
{isGridLayout ? <List className="h-4 w-4" /> : <LayoutGrid className="h-4 w-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{isGridLayout ? "Switch to list view" : "Switch to grid view"}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" title="Change view">
|
||||
{isGridLayout ? (
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
) : (
|
||||
<List className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => toggleLayout(false)}>
|
||||
<List className="h-4 w-4 mr-2" /> List View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout(true)}>
|
||||
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
onValueChange={handlePresetItemsPerPageChange}
|
||||
onOpenChange={(open) => {
|
||||
if (open && customInputRef.current) {
|
||||
customInputRef.current.value = String(itemsPerPage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![4, 6, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
<SelectItem value="4">4 items</SelectItem>
|
||||
<SelectItem value="6">6 items</SelectItem>
|
||||
<SelectItem value="10">10 items</SelectItem>
|
||||
<SelectItem value="15">15 items</SelectItem>
|
||||
<SelectItem value="20">20 items</SelectItem>
|
||||
<SelectItem value="25">25 items</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
ref={customInputRef}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
className="h-8"
|
||||
defaultValue={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
// Änderung nicht sofort anwenden während des Tippens
|
||||
// Nur visuelles Feedback für die Validierung
|
||||
const value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1 || value > 100) {
|
||||
e.target.classList.add("border-red-500");
|
||||
} else {
|
||||
e.target.classList.remove("border-red-500");
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// Änderung anwenden, wenn das Input den Fokus verliert
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
handleItemsPerPageChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Bestehenden Debounce-Timer löschen, um sofort anzuwenden
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
const value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
// Änderung sofort bei Enter anwenden
|
||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1);
|
||||
Cookies.set("itemsPerPage-servers", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Kurze Verzögerung hinzufügen für bessere Reaktionsfähigkeit
|
||||
setTimeout(() => {
|
||||
getServers();
|
||||
|
||||
// Dropdown schließen
|
||||
document.body.click();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<AlertDialog onOpenChange={setIsAddDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
@@ -858,8 +1023,13 @@ export default function Dashboard() {
|
||||
>
|
||||
<CardHeader>
|
||||
{server.monitoring && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<div className="absolute top-4 right-4 flex flex-col items-end">
|
||||
<StatusIndicator isOnline={server.online} />
|
||||
{server.online && server.uptime && (
|
||||
<span className="text-xs text-muted-foreground mt-1">
|
||||
since {server.uptime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
@@ -1862,6 +2032,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{totalItems > 0
|
||||
? `Showing ${((currentPage - 1) * itemsPerPage) + 1}-${Math.min(currentPage * itemsPerPage, totalItems)} of ${totalItems} servers`
|
||||
: "No servers found"}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
@@ -56,6 +56,7 @@ interface Server {
|
||||
diskUsage: number;
|
||||
history?: ServerHistory;
|
||||
port: number;
|
||||
uptime?: string;
|
||||
}
|
||||
|
||||
interface GetServersResponse {
|
||||
@@ -439,8 +440,13 @@ export default function ServerDetail() {
|
||||
</div>
|
||||
</div>
|
||||
{server.monitoring && (
|
||||
<div className="absolute top-0 right-4">
|
||||
<div className="absolute top-0 right-4 flex flex-col items-end">
|
||||
<StatusIndicator isOnline={server.online} />
|
||||
{server.online && server.uptime && (
|
||||
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
|
||||
since {server.uptime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
@@ -600,7 +606,14 @@ export default function ServerDetail() {
|
||||
</NextLink>
|
||||
</div>
|
||||
{hostedVM.monitoring && (
|
||||
<StatusIndicator isOnline={hostedVM.online} />
|
||||
<div className="flex flex-col items-end">
|
||||
<StatusIndicator isOnline={hostedVM.online} />
|
||||
{hostedVM.online && hostedVM.uptime && (
|
||||
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
|
||||
since {hostedVM.uptime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
+196
-186
@@ -62,6 +62,7 @@ export default function Settings() {
|
||||
const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
|
||||
|
||||
const [notificationType, setNotificationType] = useState<string>("")
|
||||
const [notificationName, setNotificationName] = useState<string>("")
|
||||
const [smtpHost, setSmtpHost] = useState<string>("")
|
||||
const [smtpPort, setSmtpPort] = useState<number>(0)
|
||||
const [smtpSecure, setSmtpSecure] = useState<boolean>(false)
|
||||
@@ -168,6 +169,7 @@ export default function Settings() {
|
||||
const addNotification = async () => {
|
||||
try {
|
||||
const response = await axios.post("/api/notifications/add", {
|
||||
name: notificationName,
|
||||
type: notificationType,
|
||||
smtpHost: smtpHost,
|
||||
smtpPort: smtpPort,
|
||||
@@ -450,212 +452,220 @@ export default function Settings() {
|
||||
<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)}
|
||||
/>
|
||||
</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="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
id="notificationName"
|
||||
placeholder="Notification Name (optional)"
|
||||
onChange={(e) => setNotificationName(e.target.value)}
|
||||
/>
|
||||
<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="smtpFrom">From Address</Label>
|
||||
<Label htmlFor="smtpHost">SMTP Host</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="smtpFrom"
|
||||
placeholder="noreply@example.com"
|
||||
onChange={(e) => setSmtpFrom(e.target.value)}
|
||||
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="smtpTo">To Address</Label>
|
||||
<Label htmlFor="smtpPass">SMTP Password</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="smtpTo"
|
||||
placeholder="admin@example.com"
|
||||
onChange={(e) => setSmtpTo(e.target.value)}
|
||||
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>
|
||||
)}
|
||||
|
||||
{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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationType === "gotify" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="gotifyUrl">Gotify URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="gotifyUrl"
|
||||
placeholder=""
|
||||
onChange={(e) => setGotifyUrl(e.target.value)}
|
||||
/>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="gotifyToken">Gotify Token</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="gotifyToken"
|
||||
placeholder=""
|
||||
onChange={(e) => setGotifyToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationType === "gotify" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="gotifyUrl">Gotify URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="gotifyUrl"
|
||||
placeholder=""
|
||||
onChange={(e) => setGotifyUrl(e.target.value)}
|
||||
/>
|
||||
{notificationType === "ntfy" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="gotifyToken">Gotify Token</Label>
|
||||
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="gotifyToken"
|
||||
id="ntfyUrl"
|
||||
placeholder=""
|
||||
onChange={(e) => setGotifyToken(e.target.value)}
|
||||
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>
|
||||
)}
|
||||
|
||||
{notificationType === "pushover" && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverUrl">Pushover URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverUrl"
|
||||
placeholder="e.g. https://api.pushover.net/1/messages.json"
|
||||
onChange={(e) => setPushoverUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverToken">Pushover Token</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverToken"
|
||||
placeholder="e.g. 1234567890"
|
||||
onChange={(e) => setPushoverToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverUser">Pushover User</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverUser"
|
||||
placeholder="e.g. 1234567890"
|
||||
onChange={(e) => setPushoverUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationType === "ntfy" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="ntfyUrl"
|
||||
placeholder=""
|
||||
onChange={(e) => setNtfyUrl(e.target.value)}
|
||||
/>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="ntfyToken">Ntfy Token</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="ntfyToken"
|
||||
placeholder=""
|
||||
onChange={(e) => setNtfyToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationType === "pushover" && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverUrl">Pushover URL</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverUrl"
|
||||
placeholder="e.g. https://api.pushover.net/1/messages.json"
|
||||
onChange={(e) => setPushoverUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverToken">Pushover Token</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverToken"
|
||||
placeholder="e.g. 1234567890"
|
||||
onChange={(e) => setPushoverToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="pushoverUser">Pushover User</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverUser"
|
||||
placeholder="e.g. 1234567890"
|
||||
onChange={(e) => setPushoverUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
@@ -765,7 +775,7 @@ export default function Settings() {
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium capitalize">{notification.type}</h3>
|
||||
<h3 className="font-medium capitalize">{notification.name && notification.name !== "" ? notification.name : notification.type}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{notification.type === "smtp" && "Email notifications"}
|
||||
{notification.type === "telegram" && "Telegram bot alerts"}
|
||||
|
||||
+165
-24
@@ -13,7 +13,6 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
@@ -26,6 +25,12 @@ import {
|
||||
PaginationNext,
|
||||
PaginationLink,
|
||||
} from "@/components/ui/pagination";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const timeFormats = {
|
||||
1: (timestamp: string) =>
|
||||
@@ -84,8 +89,16 @@ export default function Uptime() {
|
||||
totalItems: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const savedItemsPerPage = Cookies.get("itemsPerPage-uptime");
|
||||
const defaultItemsPerPage = 5;
|
||||
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
|
||||
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const getData = async (selectedTimespan: number, page: number) => {
|
||||
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await axios.post<{
|
||||
@@ -93,7 +106,8 @@ export default function Uptime() {
|
||||
pagination: PaginationData;
|
||||
}>("/api/applications/uptime", {
|
||||
timespan: selectedTimespan,
|
||||
page
|
||||
page,
|
||||
itemsPerPage
|
||||
});
|
||||
|
||||
setData(response.data.data);
|
||||
@@ -114,17 +128,48 @@ export default function Uptime() {
|
||||
const handlePrevious = () => {
|
||||
const newPage = Math.max(1, pagination.currentPage - 1);
|
||||
setPagination(prev => ({...prev, currentPage: newPage}));
|
||||
getData(timespan, newPage);
|
||||
getData(timespan, newPage, itemsPerPage);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
|
||||
setPagination(prev => ({...prev, currentPage: newPage}));
|
||||
getData(timespan, newPage);
|
||||
getData(timespan, newPage, itemsPerPage);
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (value: string) => {
|
||||
// Clear any existing timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timer
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Ensure the value is within the valid range
|
||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||
toast.error("Please enter a number between 1 and 100");
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setPagination(prev => ({...prev, currentPage: 1})); // Reset to first page
|
||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Fetch data with new pagination
|
||||
getData(timespan, 1, validatedValue);
|
||||
}, 300); // 300ms delay
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getData(timespan, 1);
|
||||
getData(timespan, 1, itemsPerPage);
|
||||
}, [timespan]);
|
||||
|
||||
return (
|
||||
@@ -152,27 +197,116 @@ export default function Uptime() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Uptime</span>
|
||||
<Select
|
||||
value={String(timespan)}
|
||||
onValueChange={(v) => {
|
||||
setTimespan(Number(v) as 1 | 2 | 3 | 4);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||
<SelectItem value="2">Last 1 day</SelectItem>
|
||||
<SelectItem value="3">Last 7 days</SelectItem>
|
||||
<SelectItem value="4">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
onValueChange={handleItemsPerPageChange}
|
||||
onOpenChange={(open) => {
|
||||
if (open && customInputRef.current) {
|
||||
customInputRef.current.value = String(itemsPerPage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
<SelectItem value="5">5 items</SelectItem>
|
||||
<SelectItem value="10">10 items</SelectItem>
|
||||
<SelectItem value="15">15 items</SelectItem>
|
||||
<SelectItem value="20">20 items</SelectItem>
|
||||
<SelectItem value="25">25 items</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
ref={customInputRef}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
className="h-8"
|
||||
defaultValue={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
// Don't immediately apply the change while typing
|
||||
// Just validate the input for visual feedback
|
||||
const value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1 || value > 100) {
|
||||
e.target.classList.add("border-red-500");
|
||||
} else {
|
||||
e.target.classList.remove("border-red-500");
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// Apply the change when the input loses focus
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
handleItemsPerPageChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Clear any existing debounce timer to apply immediately
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
const value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
// Apply change immediately on Enter
|
||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||
setItemsPerPage(validatedValue);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
getData(timespan, 1, validatedValue);
|
||||
|
||||
// Close the dropdown
|
||||
document.body.click();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={String(timespan)}
|
||||
onValueChange={(v) => {
|
||||
setTimespan(Number(v) as 1 | 2 | 3 | 4);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||
<SelectItem value="2">Last 1 day</SelectItem>
|
||||
<SelectItem value="3">Last 7 days</SelectItem>
|
||||
<SelectItem value="4">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
@@ -260,6 +394,13 @@ export default function Uptime() {
|
||||
|
||||
{pagination.totalItems > 0 && !isLoading && (
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{pagination.totalItems > 0
|
||||
? `Showing ${((pagination.currentPage - 1) * itemsPerPage) + 1}-${Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems)} of ${pagination.totalItems} items`
|
||||
: "No items found"}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
|
||||
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.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>
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{t as p}from"./chunks/theme.BTnOYcHU.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};
|
||||
import{t as p}from"./chunks/theme.9-rJywIy.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};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +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_pushover.md\":\"lZwGAQ0A\",\"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\"},{\"text\":\"Pushover\",\"link\":\"/notifications/Pushover\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");
|
||||
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\":\"BeIP42w_\",\"installation.md\":\"Cz1eOHOr\",\"notifications_discord.md\":\"C0x5CxmR\",\"notifications_email.md\":\"Cugw2BRs\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"vFHjr6ko\",\"notifications_ntfy.md\":\"CPMnGQVP\",\"notifications_pushover.md\":\"lZwGAQ0A\",\"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\"},{\"text\":\"Pushover\",\"link\":\"/notifications/Pushover\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +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};
|
||||
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 with alerts 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};
|
||||
+1
-1
@@ -1 +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};
|
||||
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 with alerts 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};
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/general_Applications.md.DFVqSlCw.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/general_Dashboard.md.DW5yESFW.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/general_Network.md.tbP8aEzX.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/general_Servers.md.BaASA60T.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/settings_notifications.DL7eQG4d.js">
|
||||
<link rel="modulepreload" href="/assets/general_Settings.md.DrC2XV32.lean.js">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/general_Uptime.md.CKBdQg4u.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"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_pushover.md":"lZwGAQ0A","notifications_telegram.md":"B6_EzaEX"}
|
||||
{"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":"BeIP42w_","installation.md":"Cz1eOHOr","notifications_discord.md":"C0x5CxmR","notifications_email.md":"Cugw2BRs","notifications_general.md":"D7AVsSjD","notifications_gotify.md":"vFHjr6ko","notifications_ntfy.md":"CPMnGQVP","notifications_pushover.md":"lZwGAQ0A","notifications_telegram.md":"B6_EzaEX"}
|
||||
|
||||
Vendored
+5
-5
File diff suppressed because one or more lines are too long
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/installation.md.Cz1eOHOr.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Discord.md.C0x5CxmR.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Email.md.Cugw2BRs.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/settings_notifications.DL7eQG4d.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_General.md.D7AVsSjD.lean.js">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Gotify.md.vFHjr6ko.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Ntfy.md.CPMnGQVP.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Pushover.md.lZwGAQ0A.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@
|
||||
<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.b7f24d28.js"></script>
|
||||
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BTnOYcHU.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/notifications_Telegram.md.B6_EzaEX.lean.js">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ features:
|
||||
details: Simple and intuitive management interface for all your needs
|
||||
- icon: 🔔
|
||||
title: Notifications
|
||||
details: Stay informed withalerts and notifications about your servers & applications status
|
||||
details: Stay informed with alerts and notifications about your servers & applications status
|
||||
- icon: ✨
|
||||
title: Clean UI
|
||||
details: Modern and user-friendly interface designed for the best user experience
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "application" ADD COLUMN "ignoreNotFoundErr" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `ignoreNotFoundErr` on the `application` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "application" DROP COLUMN "ignoreNotFoundErr";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "server" ADD COLUMN "uptime" TEXT;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `name` to the `notification` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "application" ADD COLUMN "uptimecheckUrl" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "notification" ADD COLUMN "name" TEXT NOT NULL;
|
||||
@@ -21,6 +21,7 @@ model application {
|
||||
icon String
|
||||
publicURL String
|
||||
localURL String?
|
||||
uptimecheckUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
online Boolean @default(true)
|
||||
}
|
||||
@@ -61,6 +62,7 @@ model server {
|
||||
ramUsage String?
|
||||
diskUsage String?
|
||||
online Boolean @default(true)
|
||||
uptime String?
|
||||
}
|
||||
|
||||
model settings {
|
||||
@@ -78,6 +80,7 @@ model user {
|
||||
|
||||
model notification {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
enabled Boolean @default(true)
|
||||
type String
|
||||
smtpHost String?
|
||||
|
||||
Reference in New Issue
Block a user