mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-18 16:07:10 +00:00
v0.0.6 notifications->main
v0.0.6
This commit is contained in:
commit
b70d17d844
17
README.md
17
README.md
@ -20,27 +20,30 @@ Login Page:
|
|||||||

|

|
||||||
|
|
||||||
Dashboard Page:
|
Dashboard Page:
|
||||||

|

|
||||||
|
|
||||||
Servers Page:
|
Servers Page:
|
||||||

|

|
||||||
|
|
||||||
|
VM Display:
|
||||||
|

|
||||||
|
|
||||||
Applications Page:
|
Applications Page:
|
||||||

|

|
||||||
|
|
||||||
Uptime Page:
|
Uptime Page:
|
||||||

|

|
||||||
|
|
||||||
Network Page:
|
Network Page:
|
||||||

|

|
||||||
|
|
||||||
Settings Page:
|
Settings Page:
|
||||||

|

|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- [X] Edit Applications, Applications searchbar
|
- [X] Edit Applications, Applications searchbar
|
||||||
- [X] Uptime History
|
- [X] Uptime History
|
||||||
- [ ] Notifications
|
- [X] Notifications
|
||||||
- [ ] Simple Server Monitoring
|
- [ ] Simple Server Monitoring
|
||||||
- [ ] Improved Network Flowchart with custom elements (like Network switches)
|
- [ ] Improved Network Flowchart with custom elements (like Network switches)
|
||||||
- [ ] Advanced Settings (Disable Uptime Tracking & more)
|
- [ ] Advanced Settings (Disable Uptime Tracking & more)
|
||||||
|
|||||||
@ -17,4 +17,6 @@ require (
|
|||||||
github.com/jackc/pgtype v1.14.0 // indirect
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
golang.org/x/crypto v0.20.0 // indirect
|
golang.org/x/crypto v0.20.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@ -171,9 +171,13 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
297
agent/main.go
297
agent/main.go
@ -2,22 +2,51 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/jackc/pgx/v4/stdlib"
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
ID int
|
ID int
|
||||||
|
Name string
|
||||||
PublicURL string
|
PublicURL string
|
||||||
Online bool
|
Online bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifications []Notification
|
||||||
|
notifMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
fmt.Println("No env vars found")
|
fmt.Println("No env vars found")
|
||||||
@ -34,8 +63,36 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
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() {
|
go func() {
|
||||||
deletionTicker := time.NewTicker(1 * time.Hour)
|
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()
|
defer deletionTicker.Stop()
|
||||||
|
|
||||||
for range deletionTicker.C {
|
for range deletionTicker.C {
|
||||||
@ -45,7 +102,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
@ -62,12 +119,53 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
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,
|
||||||
|
); err != nil {
|
||||||
|
fmt.Printf("Error scanning notification: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configs = append(configs, n)
|
||||||
|
}
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func deleteOldEntries(db *sql.DB) error {
|
func deleteOldEntries(db *sql.DB) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
res, err := db.ExecContext(ctx,
|
res, err := db.ExecContext(ctx,
|
||||||
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`)
|
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -77,11 +175,9 @@ func deleteOldEntries(db *sql.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getApplications(db *sql.DB) []Application {
|
func getApplications(db *sql.DB) []Application {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(
|
||||||
SELECT id, "publicURL", online
|
`SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`,
|
||||||
FROM application
|
)
|
||||||
WHERE "publicURL" IS NOT NULL
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error fetching applications: %v\n", err)
|
fmt.Printf("Error fetching applications: %v\n", err)
|
||||||
return nil
|
return nil
|
||||||
@ -91,8 +187,7 @@ func getApplications(db *sql.DB) []Application {
|
|||||||
var apps []Application
|
var apps []Application
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var app Application
|
var app Application
|
||||||
err := rows.Scan(&app.ID, &app.PublicURL, &app.Online)
|
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error scanning row: %v\n", err)
|
fmt.Printf("Error scanning row: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -102,15 +197,25 @@ func getApplications(db *sql.DB) []Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
||||||
fmt.Printf("Start checking %d applications at %v\n", len(apps), time.Now())
|
var notificationTemplate string
|
||||||
|
err := db.QueryRow("SELECT notification_text FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||||
|
if err != nil || notificationTemplate == "" {
|
||||||
|
notificationTemplate = "The application '!name' (!url) went !status!"
|
||||||
|
}
|
||||||
|
|
||||||
for i, app := range apps {
|
for _, app := range apps {
|
||||||
logPrefix := fmt.Sprintf("[App %d/%d URL: %s]", i+1, len(apps), app.PublicURL)
|
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
|
||||||
fmt.Printf("%s Starting check\n", logPrefix)
|
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())
|
||||||
|
|
||||||
startHTTP := time.Now()
|
|
||||||
httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second)
|
httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(httpCtx, "HEAD", app.PublicURL, nil)
|
req, err := http.NewRequestWithContext(httpCtx, "HEAD", app.PublicURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
||||||
@ -119,64 +224,150 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
httpDuration := time.Since(startHTTP)
|
|
||||||
|
|
||||||
if err != nil || resp == nil || (resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented) {
|
if err != nil || (resp != nil && (resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented)) {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
fmt.Printf("%s HEAD failed, trying GET fallback...\n", logPrefix)
|
fmt.Printf("%s HEAD failed, trying GET...\n", logPrefix)
|
||||||
|
|
||||||
req.Method = "GET"
|
req.Method = "GET"
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
httpDuration = time.Since(startHTTP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isOnline := false
|
var isOnline bool
|
||||||
if err == nil && resp != nil {
|
if err == nil && resp != nil {
|
||||||
isOnline = (resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 405
|
isOnline = (resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 405
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s HTTP error after %v: %v\n", logPrefix, httpDuration, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s HTTP %d after %v (ContentLength: %d)\n",
|
|
||||||
logPrefix, resp.StatusCode, httpDuration, resp.ContentLength)
|
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s HTTP error: %v\n", logPrefix, err)
|
||||||
|
|
||||||
|
// Sonderbehandlung für IP-Adressen + TLS-Zertifikatfehler
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
httpCancel()
|
httpCancel()
|
||||||
|
|
||||||
|
if isOnline != app.Online {
|
||||||
|
status := "offline"
|
||||||
|
if isOnline {
|
||||||
|
status = "online"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := notificationTemplate
|
||||||
|
message = strings.ReplaceAll(message, "!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)
|
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_, err = db.ExecContext(dbCtx,
|
||||||
startUpdate := time.Now()
|
|
||||||
updateRes, err := db.ExecContext(dbCtx,
|
|
||||||
`UPDATE application SET online = $1 WHERE id = $2`,
|
`UPDATE application SET online = $1 WHERE id = $2`,
|
||||||
isOnline,
|
isOnline, app.ID,
|
||||||
app.ID,
|
|
||||||
)
|
)
|
||||||
updateDuration := time.Since(startUpdate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s UPDATE failed after %v: %v\n", logPrefix, updateDuration, err)
|
fmt.Printf("%s DB update failed: %v\n", logPrefix, err)
|
||||||
} else {
|
|
||||||
affected, _ := updateRes.RowsAffected()
|
|
||||||
fmt.Printf("%s UPDATE OK (%d rows) after %v\n", logPrefix, affected, updateDuration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startInsert := time.Now()
|
|
||||||
insertRes, err := db.ExecContext(dbCtx,
|
|
||||||
`INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
|
||||||
app.ID,
|
|
||||||
isOnline,
|
|
||||||
)
|
|
||||||
insertDuration := time.Since(startInsert)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s INSERT failed after %v: %v\n", logPrefix, insertDuration, err)
|
|
||||||
} else {
|
|
||||||
inserted, _ := insertRes.RowsAffected()
|
|
||||||
fmt.Printf("%s INSERT OK (%d rows) after %v\n", logPrefix, inserted, insertDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbCancel()
|
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 Insert into history failed: %v\n", logPrefix, err)
|
||||||
|
}
|
||||||
|
dbCancel2()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|||||||
43
app/api/notifications/add/route.ts
Normal file
43
app/api/notifications/add/route.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface AddRequest {
|
||||||
|
type: string;
|
||||||
|
smtpHost?: string;
|
||||||
|
smtpPort?: number;
|
||||||
|
smtpSecure?: boolean;
|
||||||
|
smtpUsername?: string;
|
||||||
|
smtpPassword?: string;
|
||||||
|
smtpFrom?: string;
|
||||||
|
smtpTo?: string;
|
||||||
|
telegramToken?: string;
|
||||||
|
telegramChatId?: string;
|
||||||
|
discordWebhook?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: AddRequest = await request.json();
|
||||||
|
const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook } = body;
|
||||||
|
|
||||||
|
const notification = await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
type: type,
|
||||||
|
smtpHost: smtpHost,
|
||||||
|
smtpPort: smtpPort,
|
||||||
|
smtpFrom: smtpFrom,
|
||||||
|
smtpUser: smtpUsername,
|
||||||
|
smtpPass: smtpPassword,
|
||||||
|
smtpSecure: smtpSecure,
|
||||||
|
smtpTo: smtpTo,
|
||||||
|
telegramChatId: telegramChatId,
|
||||||
|
telegramToken: telegramToken,
|
||||||
|
discordWebhook: discordWebhook,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Success", notification });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/api/notifications/delete/route.ts
Normal file
21
app/api/notifications/delete/route.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const id = Number(body.id);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.notification.delete({
|
||||||
|
where: { id: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/api/notifications/get/route.ts
Normal file
16
app/api/notifications/get/route.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const notifications = await prisma.notification.findMany();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
notifications
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ import { NextResponse, NextRequest } from "next/server";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
interface AddRequest {
|
interface AddRequest {
|
||||||
|
host: boolean;
|
||||||
|
hostServer: number;
|
||||||
name: string;
|
name: string;
|
||||||
os: string;
|
os: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
@ -16,10 +18,12 @@ interface AddRequest {
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body: AddRequest = await request.json();
|
const body: AddRequest = await request.json();
|
||||||
const { name, os, ip, url, cpu, gpu, ram, disk } = body;
|
const { host, hostServer, name, os, ip, url, cpu, gpu, ram, disk } = body;
|
||||||
|
|
||||||
const server = await prisma.server.create({
|
const server = await prisma.server.create({
|
||||||
data: {
|
data: {
|
||||||
|
host,
|
||||||
|
hostServer,
|
||||||
name,
|
name,
|
||||||
os,
|
os,
|
||||||
ip,
|
ip,
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { NextResponse, NextRequest } from "next/server";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
interface EditRequest {
|
interface EditRequest {
|
||||||
|
host: boolean;
|
||||||
|
hostServer: number;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
os: string;
|
os: string;
|
||||||
@ -16,7 +18,7 @@ interface EditRequest {
|
|||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body: EditRequest = await request.json();
|
const body: EditRequest = await request.json();
|
||||||
const { id, name, os, ip, url, cpu, gpu, ram, disk } = body;
|
const { host, hostServer, id, name, os, ip, url, cpu, gpu, ram, disk } = body;
|
||||||
|
|
||||||
const existingServer = await prisma.server.findUnique({ where: { id } });
|
const existingServer = await prisma.server.findUnique({ where: { id } });
|
||||||
if (!existingServer) {
|
if (!existingServer) {
|
||||||
@ -26,6 +28,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
const updatedServer = await prisma.server.update({
|
const updatedServer = await prisma.server.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
host,
|
||||||
|
hostServer,
|
||||||
name,
|
name,
|
||||||
os,
|
os,
|
||||||
ip,
|
ip,
|
||||||
|
|||||||
@ -6,24 +6,37 @@ interface GetRequest {
|
|||||||
ITEMS_PER_PAGE?: number;
|
ITEMS_PER_PAGE?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body: GetRequest = await request.json();
|
const body: GetRequest = await request.json();
|
||||||
const page = Math.max(1, body.page || 1);
|
const page = Math.max(1, body.page || 1);
|
||||||
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
|
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
|
||||||
|
|
||||||
const servers = await prisma.server.findMany({
|
const hosts = await prisma.server.findMany({
|
||||||
|
where: { hostServer: 0 },
|
||||||
skip: (page - 1) * ITEMS_PER_PAGE,
|
skip: (page - 1) * ITEMS_PER_PAGE,
|
||||||
take: ITEMS_PER_PAGE,
|
take: ITEMS_PER_PAGE,
|
||||||
orderBy: { name: 'asc' }
|
orderBy: { name: 'asc' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await prisma.server.count();
|
const hostsWithVms = await Promise.all(
|
||||||
const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
|
hosts.map(async (host) => ({
|
||||||
|
...host,
|
||||||
|
hostedVMs: await prisma.server.findMany({
|
||||||
|
where: { hostServer: host.id },
|
||||||
|
orderBy: { name: 'asc' }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalHosts = await prisma.server.count({
|
||||||
|
where: { hostServer: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
servers,
|
servers: hostsWithVms,
|
||||||
maxPage
|
maxPage
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
13
app/api/servers/hosts/route.ts
Normal file
13
app/api/servers/hosts/route.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const servers = await prisma.server.findMany({
|
||||||
|
where: { host: true },
|
||||||
|
});
|
||||||
|
return NextResponse.json({ servers });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/api/settings/get_notification_text/route.ts
Normal file
25
app/api/settings/get_notification_text/route.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check if there are any settings entries
|
||||||
|
const existingSettings = await prisma.settings.findFirst();
|
||||||
|
if (!existingSettings) {
|
||||||
|
return NextResponse.json({ "notification_text": "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If settings entry exists, fetch it
|
||||||
|
const settings = await prisma.settings.findFirst({
|
||||||
|
where: { id: existingSettings.id },
|
||||||
|
});
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json({ "notification_text": "" });
|
||||||
|
}
|
||||||
|
// Return the settings entry
|
||||||
|
return NextResponse.json({ "notification_text": settings.notification_text });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/api/settings/notification_text/route.ts
Normal file
34
app/api/settings/notification_text/route.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface AddRequest {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: AddRequest = await request.json();
|
||||||
|
const { text } = body;
|
||||||
|
|
||||||
|
// Check if there is already a settings entry
|
||||||
|
const existingSettings = await prisma.settings.findFirst();
|
||||||
|
if (existingSettings) {
|
||||||
|
// Update the existing settings entry
|
||||||
|
const updatedSettings = await prisma.settings.update({
|
||||||
|
where: { id: existingSettings.id },
|
||||||
|
data: { notification_text: text },
|
||||||
|
});
|
||||||
|
return NextResponse.json({ message: "Success", updatedSettings });
|
||||||
|
}
|
||||||
|
// If no settings entry exists, create a new one
|
||||||
|
const settings = await prisma.settings.create({
|
||||||
|
data: {
|
||||||
|
notification_text: text,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Success", settings });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ import {
|
|||||||
Microchip,
|
Microchip,
|
||||||
MemoryStick,
|
MemoryStick,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
|
Server,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -77,10 +78,15 @@ import Cookies from "js-cookie";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Alert } from "@/components/ui/alert";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
host: boolean;
|
||||||
|
hostServer: number | null;
|
||||||
os?: string;
|
os?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
@ -88,6 +94,7 @@ interface Server {
|
|||||||
gpu?: string;
|
gpu?: string;
|
||||||
ram?: string;
|
ram?: string;
|
||||||
disk?: string;
|
disk?: string;
|
||||||
|
hostedVMs: Server[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetServersResponse {
|
interface GetServersResponse {
|
||||||
@ -96,6 +103,8 @@ interface GetServersResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
const [host, setHost] = useState<boolean>(false);
|
||||||
|
const [hostServer, setHostServer] = useState<number>(0);
|
||||||
const [name, setName] = useState<string>("");
|
const [name, setName] = useState<string>("");
|
||||||
const [os, setOs] = useState<string>("");
|
const [os, setOs] = useState<string>("");
|
||||||
const [ip, setIp] = useState<string>("");
|
const [ip, setIp] = useState<string>("");
|
||||||
@ -113,6 +122,8 @@ export default function Dashboard() {
|
|||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const [editId, setEditId] = useState<number | null>(null);
|
const [editId, setEditId] = useState<number | null>(null);
|
||||||
|
const [editHost, setEditHost] = useState<boolean>(false);
|
||||||
|
const [editHostServer, setEditHostServer] = useState<number | null>(0);
|
||||||
const [editName, setEditName] = useState<string>("");
|
const [editName, setEditName] = useState<string>("");
|
||||||
const [editOs, setEditOs] = useState<string>("");
|
const [editOs, setEditOs] = useState<string>("");
|
||||||
const [editIp, setEditIp] = useState<string>("");
|
const [editIp, setEditIp] = useState<string>("");
|
||||||
@ -125,6 +136,9 @@ export default function Dashboard() {
|
|||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [isSearching, setIsSearching] = useState<boolean>(false);
|
const [isSearching, setIsSearching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [hostServers, setHostServers] = useState<Server[]>([]);
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedLayout = Cookies.get("layoutPreference-servers");
|
const savedLayout = Cookies.get("layoutPreference-servers");
|
||||||
const layout_bool = savedLayout === "grid";
|
const layout_bool = savedLayout === "grid";
|
||||||
@ -146,6 +160,8 @@ export default function Dashboard() {
|
|||||||
const add = async () => {
|
const add = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.post("/api/servers/add", {
|
await axios.post("/api/servers/add", {
|
||||||
|
host,
|
||||||
|
hostServer,
|
||||||
name,
|
name,
|
||||||
os,
|
os,
|
||||||
ip,
|
ip,
|
||||||
@ -155,6 +171,18 @@ export default function Dashboard() {
|
|||||||
ram,
|
ram,
|
||||||
disk,
|
disk,
|
||||||
});
|
});
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
setHost(false);
|
||||||
|
setHostServer(0);
|
||||||
|
|
||||||
|
setName("");
|
||||||
|
setOs("");
|
||||||
|
setIp("");
|
||||||
|
setUrl("");
|
||||||
|
setCpu("");
|
||||||
|
setGpu("");
|
||||||
|
setRam("");
|
||||||
|
setDisk("");
|
||||||
getServers();
|
getServers();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response.data);
|
console.log(error.response.data);
|
||||||
@ -171,6 +199,10 @@ export default function Dashboard() {
|
|||||||
ITEMS_PER_PAGE: itemsPerPage,
|
ITEMS_PER_PAGE: itemsPerPage,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
for (const server of response.data.servers) {
|
||||||
|
console.log("Host Server:" + server.hostServer);
|
||||||
|
console.log("ID:" + server.id);
|
||||||
|
}
|
||||||
setServers(response.data.servers);
|
setServers(response.data.servers);
|
||||||
setMaxPage(response.data.maxPage);
|
setMaxPage(response.data.maxPage);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -202,6 +234,8 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const openEditDialog = (server: Server) => {
|
const openEditDialog = (server: Server) => {
|
||||||
setEditId(server.id);
|
setEditId(server.id);
|
||||||
|
setEditHost(server.host);
|
||||||
|
setEditHostServer(server.hostServer || null);
|
||||||
setEditName(server.name);
|
setEditName(server.name);
|
||||||
setEditOs(server.os || "");
|
setEditOs(server.os || "");
|
||||||
setEditIp(server.ip || "");
|
setEditIp(server.ip || "");
|
||||||
@ -218,6 +252,8 @@ export default function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
await axios.put("/api/servers/edit", {
|
await axios.put("/api/servers/edit", {
|
||||||
id: editId,
|
id: editId,
|
||||||
|
host: editHost,
|
||||||
|
hostServer: editHostServer,
|
||||||
name: editName,
|
name: editName,
|
||||||
os: editOs,
|
os: editOs,
|
||||||
ip: editIp,
|
ip: editIp,
|
||||||
@ -261,6 +297,23 @@ export default function Dashboard() {
|
|||||||
return () => clearTimeout(delayDebounce);
|
return () => clearTimeout(delayDebounce);
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchHostServers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<{ servers: Server[] }>(
|
||||||
|
"/api/servers/hosts"
|
||||||
|
);
|
||||||
|
setHostServers(response.data.servers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching host servers:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAddDialogOpen || editId !== null) {
|
||||||
|
fetchHostServers();
|
||||||
|
}
|
||||||
|
}, [isAddDialogOpen, editId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
@ -312,7 +365,7 @@ export default function Dashboard() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<AlertDialog>
|
<AlertDialog onOpenChange={setIsAddDialogOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<Plus />
|
<Plus />
|
||||||
@ -326,6 +379,9 @@ export default function Dashboard() {
|
|||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="hardware">Hardware</TabsTrigger>
|
<TabsTrigger value="hardware">Hardware</TabsTrigger>
|
||||||
|
<TabsTrigger value="virtualization">
|
||||||
|
Virtualization
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general">
|
<TabsContent value="general">
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
@ -459,6 +515,47 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="virtualization">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="hostCheckbox"
|
||||||
|
checked={host}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setHost(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="hostCheckbox">
|
||||||
|
Mark as host server
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{!host && (
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label>Host Server</Label>
|
||||||
|
<Select
|
||||||
|
value={hostServer?.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setHostServer(Number(value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a host server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{hostServers.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.id}
|
||||||
|
value={server.id.toString()}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
@ -487,7 +584,9 @@ export default function Dashboard() {
|
|||||||
: "space-y-4"
|
: "space-y-4"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{servers.map((server) => (
|
{servers
|
||||||
|
.filter((server) => server.hostServer === 0)
|
||||||
|
.map((server) => (
|
||||||
<Card
|
<Card
|
||||||
key={server.id}
|
key={server.id}
|
||||||
className={
|
className={
|
||||||
@ -606,9 +705,26 @@ export default function Dashboard() {
|
|||||||
<TabsTrigger value="hardware">
|
<TabsTrigger value="hardware">
|
||||||
Hardware
|
Hardware
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="virtualization">
|
||||||
|
Virtualization
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general">
|
<TabsContent value="general">
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editName">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editName"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Server1"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditName(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="editOs">
|
<Label htmlFor="editOs">
|
||||||
Operating System
|
Operating System
|
||||||
@ -667,7 +783,9 @@ export default function Dashboard() {
|
|||||||
<TabsContent value="hardware">
|
<TabsContent value="hardware">
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="editCpu">CPU</Label>
|
<Label htmlFor="editCpu">
|
||||||
|
CPU
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="editCpu"
|
id="editCpu"
|
||||||
value={editCpu}
|
value={editCpu}
|
||||||
@ -677,7 +795,9 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="editGpu">GPU</Label>
|
<Label htmlFor="editGpu">
|
||||||
|
GPU
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="editGpu"
|
id="editGpu"
|
||||||
value={editGpu}
|
value={editGpu}
|
||||||
@ -687,7 +807,9 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="editRam">RAM</Label>
|
<Label htmlFor="editRam">
|
||||||
|
RAM
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="editRam"
|
id="editRam"
|
||||||
value={editRam}
|
value={editRam}
|
||||||
@ -710,15 +832,495 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="virtualization">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="editHostCheckbox"
|
||||||
|
checked={editHost}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setEditHost(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="editHostCheckbox">
|
||||||
|
Mark as host server
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{!editHost && (
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label>Host Server</Label>
|
||||||
|
<Select
|
||||||
|
value={editHostServer?.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setEditHostServer(
|
||||||
|
Number(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a host server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{hostServers.map(
|
||||||
|
(server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.id}
|
||||||
|
value={server.id.toString()}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
<Button onClick={edit}>Save</Button>
|
<Button onClick={edit}>Save</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{server.hostedVMs.length > 0 && (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
>
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Hosted VMs
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{server.host && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<ScrollArea className="h-[500px] w-fzull rounded-md border p-4">
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{server.hostedVMs?.map(
|
||||||
|
(hostedVM) => (
|
||||||
|
<div
|
||||||
|
key={hostedVM.id}
|
||||||
|
className="flex flex-col gap-2 border border-muted py-2 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-base font-extrabold">
|
||||||
|
{hostedVM.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
hostedVM.url,
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() =>
|
||||||
|
deleteApplication(
|
||||||
|
hostedVM.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() =>
|
||||||
|
openEditDialog(
|
||||||
|
hostedVM
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Edit VM
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Tabs
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger value="general">
|
||||||
|
General
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="hardware">
|
||||||
|
Hardware
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="virtualization">
|
||||||
|
Virtualization
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="general">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editName">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editName"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Server1"
|
||||||
|
value={
|
||||||
|
editName
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditName(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editOs">
|
||||||
|
Operating
|
||||||
|
System
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
editOs
|
||||||
|
}
|
||||||
|
onValueChange={
|
||||||
|
setEditOs
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select OS" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Windows">
|
||||||
|
Windows
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Linux">
|
||||||
|
Linux
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="MacOS">
|
||||||
|
MacOS
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editIp">
|
||||||
|
IP
|
||||||
|
Adress
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editIp"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 192.168.100.2"
|
||||||
|
value={
|
||||||
|
editIp
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditIp(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editUrl">
|
||||||
|
Management
|
||||||
|
URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editUrl"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. https://proxmox.server1.com"
|
||||||
|
value={
|
||||||
|
editUrl
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditUrl(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="hardware">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editCpu">
|
||||||
|
CPU
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editCpu"
|
||||||
|
value={
|
||||||
|
editCpu
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditCpu(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editGpu">
|
||||||
|
GPU
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editGpu"
|
||||||
|
value={
|
||||||
|
editGpu
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditGpu(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editRam">
|
||||||
|
RAM
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editRam"
|
||||||
|
value={
|
||||||
|
editRam
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditRam(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="editDisk">
|
||||||
|
Disk
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="editDisk"
|
||||||
|
value={
|
||||||
|
editDisk
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setEditDisk(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="virtualization">
|
||||||
|
<div className="space-y-4 pt-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="editHostCheckbox"
|
||||||
|
checked={
|
||||||
|
editHost
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) =>
|
||||||
|
setEditHost(
|
||||||
|
checked ===
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="editHostCheckbox">
|
||||||
|
Mark as
|
||||||
|
host
|
||||||
|
server
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{!editHost && (
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label>
|
||||||
|
Host
|
||||||
|
Server
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={editHostServer?.toString()}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
setEditHostServer(
|
||||||
|
Number(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a host server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{hostServers.map(
|
||||||
|
(
|
||||||
|
server
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
server.id
|
||||||
|
}
|
||||||
|
value={server.id.toString()}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
server.name
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<Button
|
||||||
|
onClick={edit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-fullpb-2">
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-5 pb-2">
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<MonitorCog className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>OS:</b>{" "}
|
||||||
|
{hostedVM.os || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<FileDigit className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>IP:</b>{" "}
|
||||||
|
{hostedVM.ip ||
|
||||||
|
"Not set"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>CPU:</b>{" "}
|
||||||
|
{hostedVM.cpu || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<Microchip className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>GPU:</b>{" "}
|
||||||
|
{hostedVM.gpu || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>RAM:</b>{" "}
|
||||||
|
{hostedVM.ram || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<b>Disk:</b>{" "}
|
||||||
|
{hostedVM.disk || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
Close
|
||||||
|
</AlertDialogCancel>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -762,7 +1364,9 @@ export default function Dashboard() {
|
|||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
isActive={currentPage > 1}
|
isActive={currentPage > 1}
|
||||||
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
style={{
|
||||||
|
cursor: currentPage === 1 ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
|
|
||||||
@ -774,7 +1378,10 @@ export default function Dashboard() {
|
|||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
isActive={currentPage < maxPage}
|
isActive={currentPage < maxPage}
|
||||||
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
|
style={{
|
||||||
|
cursor:
|
||||||
|
currentPage === maxPage ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
|
|||||||
@ -21,19 +21,34 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { AlertCircle, Check, Palette, User } from "lucide-react";
|
import { AlertCircle, Check, Palette, User, Bell } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
interface NotificationsResponse {
|
||||||
|
notifications: any[];
|
||||||
|
}
|
||||||
|
interface NotificationResponse {
|
||||||
|
notification_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@ -51,6 +66,22 @@ export default function Settings() {
|
|||||||
const [passwordSuccess, setPasswordSuccess] = useState<boolean>(false)
|
const [passwordSuccess, setPasswordSuccess] = useState<boolean>(false)
|
||||||
const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
|
const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [notificationType, setNotificationType] = useState<string>("")
|
||||||
|
const [smtpHost, setSmtpHost] = useState<string>("")
|
||||||
|
const [smtpPort, setSmtpPort] = useState<number>(0)
|
||||||
|
const [smtpSecure, setSmtpSecure] = useState<boolean>(false)
|
||||||
|
const [smtpUsername, setSmtpUsername] = useState<string>("")
|
||||||
|
const [smtpPassword, setSmtpPassword] = useState<string>("")
|
||||||
|
const [smtpFrom, setSmtpFrom] = useState<string>("")
|
||||||
|
const [smtpTo, setSmtpTo] = useState<string>("")
|
||||||
|
const [telegramToken, setTelegramToken] = useState<string>("")
|
||||||
|
const [telegramChatId, setTelegramChatId] = useState<string>("")
|
||||||
|
const [discordWebhook, setDiscordWebhook] = useState<string>("")
|
||||||
|
|
||||||
|
const [notifications, setNotifications] = useState<any[]>([])
|
||||||
|
|
||||||
|
const [notificationText, setNotificationText] = useState<string>("")
|
||||||
|
|
||||||
const changeEmail = async () => {
|
const changeEmail = async () => {
|
||||||
setEmailErrorVisible(false);
|
setEmailErrorVisible(false);
|
||||||
setEmailSuccess(false);
|
setEmailSuccess(false);
|
||||||
@ -131,6 +162,88 @@ export default function Settings() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addNotification = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/notifications/add', {
|
||||||
|
type: notificationType,
|
||||||
|
smtpHost: smtpHost,
|
||||||
|
smtpPort: smtpPort,
|
||||||
|
smtpSecure: smtpSecure,
|
||||||
|
smtpUsername: smtpUsername,
|
||||||
|
smtpPassword: smtpPassword,
|
||||||
|
smtpFrom: smtpFrom,
|
||||||
|
smtpTo: smtpTo,
|
||||||
|
telegramToken: telegramToken,
|
||||||
|
telegramChatId: telegramChatId,
|
||||||
|
discordWebhook: discordWebhook
|
||||||
|
});
|
||||||
|
getNotifications();
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
alert(error.response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNotification = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/notifications/delete', {
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
getNotifications()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNotifications = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<NotificationsResponse>('/api/notifications/get', {});
|
||||||
|
if (response.status === 200 && response.data) {
|
||||||
|
setNotifications(response.data.notifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
alert(error.response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNotifications()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const getNotificationText = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<NotificationResponse>('/api/settings/get_notification_text', {});
|
||||||
|
if (response.status === 200) {
|
||||||
|
if (response.data.notification_text) {
|
||||||
|
setNotificationText(response.data.notification_text);
|
||||||
|
} else {
|
||||||
|
setNotificationText("The application !name (!url) is now !status.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editNotificationText = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/settings/notification_text', {
|
||||||
|
text: notificationText
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNotificationText()
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
@ -289,6 +402,213 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||||
|
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
|
<h2 className="text-xl font-semibold">Notifications</h2>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-6">
|
||||||
|
<div className="text-sm text-muted-foreground mb-6">
|
||||||
|
Set up Notifications to get notified when an application goes offline or online.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button className="w-full">
|
||||||
|
Add Notification
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogTitle>Add Notification</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Notification Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="smtp">SMTP</SelectItem>
|
||||||
|
<SelectItem value="telegram">Telegram</SelectItem>
|
||||||
|
<SelectItem value="discord">Discord</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
|
||||||
|
{notificationType === "smtp" && (
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpHost">SMTP Host</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="smtpHost"
|
||||||
|
placeholder="smtp.example.com"
|
||||||
|
onChange={(e) => setSmtpHost(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpPort">SMTP Port</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="smtpPort"
|
||||||
|
placeholder="587"
|
||||||
|
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 pt-2 pb-4">
|
||||||
|
<Checkbox
|
||||||
|
id="smtpSecure"
|
||||||
|
onCheckedChange={(checked: any) => setSmtpSecure(checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
|
||||||
|
Secure Connection (TLS/SSL)
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpUser">SMTP Username</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="smtpUser"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
onChange={(e) => setSmtpUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpPass">SMTP Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
id="smtpPass"
|
||||||
|
placeholder="••••••••"
|
||||||
|
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpFrom">From Address</Label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
id="smtpFrom"
|
||||||
|
placeholder="noreply@example.com"
|
||||||
|
onChange={(e) => setSmtpFrom(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpTo">To Address</Label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
id="smtpTo"
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
onChange={(e) => setSmtpTo(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Select>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={addNotification}>
|
||||||
|
Add
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<div className="pt-4 pb-2">
|
||||||
|
<Button className="w-full" variant="secondary">
|
||||||
|
Customize Notification Text
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="text">Notification Text</Label>
|
||||||
|
<Textarea id="text" placeholder="Type here..." value={notificationText} onChange={(e) => setNotificationText(e.target.value)} rows={4} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 text-sm text-muted-foreground">
|
||||||
|
You can use the following placeholders in the text:
|
||||||
|
<ul className="list-disc list-inside space-y-1 pt-2">
|
||||||
|
<li><strong>!name</strong> - Application name</li>
|
||||||
|
<li><strong>!url</strong> - Application URL</li>
|
||||||
|
<li><strong>!status</strong> - Application status (online/offline)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={editNotificationText}>
|
||||||
|
Save
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
{notifications.length > 0 ? (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className="flex items-center justify-between p-4 bg-muted/10 rounded-lg border"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium capitalize">{notification.type}</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteNotification(notification.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-6">
|
||||||
|
No notifications configured
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|||||||
@ -1,10 +1,25 @@
|
|||||||
import * as React from "react"
|
"use client"
|
||||||
|
|
||||||
|
import type * as React from "react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network, Activity } from "lucide-react"
|
import { usePathname } from "next/navigation"
|
||||||
|
import {
|
||||||
|
AppWindow,
|
||||||
|
Settings,
|
||||||
|
LayoutDashboardIcon,
|
||||||
|
Briefcase,
|
||||||
|
Server,
|
||||||
|
Network,
|
||||||
|
Activity,
|
||||||
|
LogOut,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react"
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
@ -13,12 +28,15 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
|
SidebarFooter,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import packageJson from "@/package.json"
|
import packageJson from "@/package.json"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
title: string
|
title: string
|
||||||
@ -33,7 +51,7 @@ const data: { navMain: NavItem[] } = {
|
|||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
icon: LayoutDashboardIcon,
|
icon: LayoutDashboardIcon,
|
||||||
url: "/dashboard"
|
url: "/dashboard",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "My Infrastructure",
|
title: "My Infrastructure",
|
||||||
@ -72,23 +90,38 @@ const data: { navMain: NavItem[] } = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
Cookies.remove('token')
|
Cookies.remove("token")
|
||||||
router.push("/")
|
router.push("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a path is active (exact match or starts with path for parent items)
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === "#") return false
|
||||||
|
return pathname === path || (path !== "/dashboard" && pathname?.startsWith(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any child item is active
|
||||||
|
const hasActiveChild = (items?: NavItem[]) => {
|
||||||
|
if (!items) return false
|
||||||
|
return items.some((item) => isActive(item.url))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar {...props}>
|
<Sidebar {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader className="border-b border-sidebar-border/30 pb-2">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton size="lg" asChild>
|
<SidebarMenuButton size="lg" asChild className="gap-3">
|
||||||
<a href="https://github.com/crocofied/corecontrol">
|
<a href="https://github.com/crocofied/corecontrol" className="transition-all hover:opacity-80">
|
||||||
<Image src="/logo.png" width={48} height={48} alt="Logo"/>
|
<div className="flex items-center justify-center rounded-lg overflow-hidden bg-gradient-to-br from-teal-500 to-emerald-600 shadow-sm">
|
||||||
|
<Image src="/logo.png" width={48} height={48} alt="CoreControl Logo" className="object-cover" />
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 leading-none">
|
<div className="flex flex-col gap-0.5 leading-none">
|
||||||
<span className="font-semibold">CoreControl</span>
|
<span className="font-semibold text-base">CoreControl</span>
|
||||||
<span className="">v{packageJson.version}</span>
|
<span className="text-xs text-sidebar-foreground/70">v{packageJson.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@ -96,44 +129,77 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent className="flex flex-col h-full">
|
<SidebarContent className="flex flex-col h-full py-4">
|
||||||
<SidebarGroup className="flex-grow">
|
<SidebarGroup className="flex-grow">
|
||||||
|
<SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2">
|
||||||
|
Main Navigation
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{data.navMain.map((item) => (
|
{data.navMain.map((item) =>
|
||||||
<SidebarMenuItem key={item.title}>
|
item.items?.length ? (
|
||||||
<SidebarMenuButton asChild>
|
<Collapsible key={item.title} defaultOpen={hasActiveChild(item.items)} className="group/collapsible">
|
||||||
<Link href={item.url} className="font-medium">
|
<SidebarMenuItem>
|
||||||
{item.icon && <item.icon className="mr-2" />}
|
<CollapsibleTrigger asChild>
|
||||||
{item.title}
|
<SidebarMenuButton
|
||||||
</Link>
|
className={cn(
|
||||||
|
"font-medium transition-all",
|
||||||
|
(hasActiveChild(item.items) || isActive(item.url)) &&
|
||||||
|
"text-sidebar-accent-foreground bg-sidebar-accent/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.icon && <item.icon className="h-4 w-4" />}
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
{item.items?.length && (
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{item.items.map((subItem) => (
|
{item.items.map((subItem) => (
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton asChild isActive={isActive(subItem.url)} className="transition-all">
|
||||||
asChild
|
<Link href={subItem.url} className="flex items-center">
|
||||||
isActive={subItem.isActive ?? false}
|
{subItem.icon && <subItem.icon className="h-3.5 w-3.5 mr-2" />}
|
||||||
>
|
<span>{subItem.title}</span>
|
||||||
<Link href={subItem.url}>
|
|
||||||
{subItem.icon && <subItem.icon className="mr-2" />}
|
|
||||||
{subItem.title}
|
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
)}
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
</Collapsible>
|
||||||
|
) : (
|
||||||
|
<SidebarMenuItem key={item.title}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
asChild
|
||||||
|
className={cn(
|
||||||
|
"font-medium transition-all",
|
||||||
|
isActive(item.url) && "text-sidebar-accent-foreground bg-sidebar-accent/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link href={item.url}>
|
||||||
|
{item.icon && <item.icon className="h-4 w-4" />}
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
<div className="p-4">
|
<SidebarFooter className="border-t border-sidebar-border/30 pt-4 mt-auto">
|
||||||
<Button variant="destructive" className="w-full" onClick={logout}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-destructive hover:text-destructive hover:bg-destructive/10 border-none shadow-none"
|
||||||
|
onClick={logout}
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SidebarFooter>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|||||||
46
components/ui/badge.tsx
Normal file
46
components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
32
components/ui/checkbox.tsx
Normal file
32
components/ui/checkbox.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="flex items-center justify-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
33
components/ui/collapsible.tsx
Normal file
33
components/ui/collapsible.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
58
components/ui/scroll-area.tsx
Normal file
58
components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
@ -14,6 +14,9 @@ services:
|
|||||||
image: haedlessdev/corecontrol-agent:latest
|
image: haedlessdev/corecontrol-agent:latest
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:17
|
image: postgres:17
|
||||||
@ -24,6 +27,11 @@ services:
|
|||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
83
package-lock.json
generated
83
package-lock.json
generated
@ -1,20 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "corecontrol",
|
"name": "corecontrol",
|
||||||
"version": "0.0.4",
|
"version": "0.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "corecontrol",
|
"name": "corecontrol",
|
||||||
"version": "0.0.4",
|
"version": "0.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"@prisma/extension-accelerate": "^1.3.0",
|
"@prisma/extension-accelerate": "^1.3.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.5",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.7",
|
"@radix-ui/react-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||||
"@radix-ui/react-label": "^2.1.3",
|
"@radix-ui/react-label": "^2.1.3",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.4",
|
||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.3",
|
"@radix-ui/react-separator": "^1.1.3",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
@ -1240,6 +1243,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-B0gYIVxl77KYDR25AY9EGe/G//ef85RVBIxQvK+m5pxAC7XihAc/8leMHhDvjvhDu02SBSb6BuytlWr/G7F3+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.3",
|
||||||
|
"@radix-ui/react-primitive": "2.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collapsible": {
|
"node_modules/@radix-ui/react-collapsible": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz",
|
||||||
@ -1688,6 +1721,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.3",
|
||||||
|
"@radix-ui/react-primitive": "2.0.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
|
||||||
@ -4545,6 +4609,21 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "15.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
|
||||||
|
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "corecontrol",
|
"name": "corecontrol",
|
||||||
"version": "0.0.4",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
@ -13,9 +13,12 @@
|
|||||||
"@prisma/extension-accelerate": "^1.3.0",
|
"@prisma/extension-accelerate": "^1.3.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.5",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.7",
|
"@radix-ui/react-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||||
"@radix-ui/react-label": "^2.1.3",
|
"@radix-ui/react-label": "^2.1.3",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.4",
|
||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.3",
|
"@radix-ui/react-separator": "^1.1.3",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "notification" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"smtpHost" TEXT,
|
||||||
|
"smtpPort" INTEGER,
|
||||||
|
"smtpFrom" TEXT,
|
||||||
|
"smtpUser" TEXT,
|
||||||
|
"smtpPass" TEXT,
|
||||||
|
"smtpSecure" BOOLEAN,
|
||||||
|
"smtpTo" TEXT,
|
||||||
|
"telegramChatId" TEXT,
|
||||||
|
"telegramToken" TEXT,
|
||||||
|
"discordWebhook" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "notification_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "settings" ADD COLUMN "notification_text" TEXT;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "server" ADD COLUMN "host" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "hostServer" TEXT;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The `hostServer` column on the `server` table would be dropped and recreated. This will lead to data loss if there is data in the column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "server" DROP COLUMN "hostServer",
|
||||||
|
ADD COLUMN "hostServer" INTEGER;
|
||||||
@ -34,6 +34,8 @@ model uptime_history {
|
|||||||
|
|
||||||
model server {
|
model server {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
host Boolean @default(false)
|
||||||
|
hostServer Int?
|
||||||
name String
|
name String
|
||||||
os String?
|
os String?
|
||||||
ip String?
|
ip String?
|
||||||
@ -47,6 +49,7 @@ model server {
|
|||||||
model settings {
|
model settings {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
uptime_checks Boolean @default(true)
|
uptime_checks Boolean @default(true)
|
||||||
|
notification_text String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model user {
|
model user {
|
||||||
@ -54,3 +57,19 @@ model user {
|
|||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model notification {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
type String
|
||||||
|
smtpHost String?
|
||||||
|
smtpPort Int?
|
||||||
|
smtpFrom String?
|
||||||
|
smtpUser String?
|
||||||
|
smtpPass String?
|
||||||
|
smtpSecure Boolean?
|
||||||
|
smtpTo String?
|
||||||
|
telegramChatId String?
|
||||||
|
telegramToken String?
|
||||||
|
discordWebhook String?
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user