mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-24 10:56:54 +00:00
v0.0.10 -> main
V0.0.10
This commit is contained in:
commit
51bd61622e
BIN
ApplicationsFormatted.tsx
Normal file
BIN
ApplicationsFormatted.tsx
Normal file
Binary file not shown.
@ -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"]
|
||||
|
||||
111
agent/cmd/agent/main.go
Normal file
111
agent/cmd/agent/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
14
agent/go.mod
14
agent/go.mod
@ -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
|
||||
)
|
||||
|
||||
39
agent/go.sum
39
agent/go.sum
@ -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=
|
||||
|
||||
130
agent/internal/app/monitor.go
Normal file
130
agent/internal/app/monitor.go
Normal file
@ -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
|
||||
}
|
||||
198
agent/internal/database/database.go
Normal file
198
agent/internal/database/database.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
agent/internal/models/models.go
Normal file
75
agent/internal/models/models.go
Normal file
@ -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
|
||||
}
|
||||
239
agent/internal/notifications/notifications.go
Normal file
239
agent/internal/notifications/notifications.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
291
agent/internal/server/monitor.go
Normal file
291
agent/internal/server/monitor.go
Normal file
@ -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
agent/main.go
805
agent/main.go
@ -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>
|
||||
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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>
|
||||
|
||||
4
docs/.vitepress/dist/404.html
vendored
4
docs/.vitepress/dist/404.html
vendored
@ -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 @@
|
||||
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};
|
||||
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BgQKcov9.js
vendored
Normal file
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BgQKcov9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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}");
|
||||
File diff suppressed because one or more lines are too long
@ -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 @@
|
||||
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};
|
||||
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/general/Dashboard.html
vendored
6
docs/.vitepress/dist/general/Dashboard.html
vendored
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/general/Network.html
vendored
6
docs/.vitepress/dist/general/Network.html
vendored
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/general/Servers.html
vendored
6
docs/.vitepress/dist/general/Servers.html
vendored
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/general/Settings.html
vendored
6
docs/.vitepress/dist/general/Settings.html
vendored
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/general/Uptime.html
vendored
6
docs/.vitepress/dist/general/Uptime.html
vendored
@ -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">
|
||||
|
||||
2
docs/.vitepress/dist/hashmap.json
vendored
2
docs/.vitepress/dist/hashmap.json
vendored
@ -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"}
|
||||
|
||||
10
docs/.vitepress/dist/index.html
vendored
10
docs/.vitepress/dist/index.html
vendored
File diff suppressed because one or more lines are too long
6
docs/.vitepress/dist/installation.html
vendored
6
docs/.vitepress/dist/installation.html
vendored
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
6
docs/.vitepress/dist/notifications/Ntfy.html
vendored
6
docs/.vitepress/dist/notifications/Ntfy.html
vendored
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
@ -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?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user