Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3eabb9297 | ||
|
|
111864d0b3 | ||
|
|
343dc15f66 | ||
|
|
f297e20ae1 | ||
|
|
09bdf49d0f | ||
|
|
088f46df29 | ||
|
|
bd61e665df | ||
|
|
c3df67f407 | ||
|
|
08cce9fd24 | ||
|
|
4e64701ddf | ||
|
|
512a5aa49e | ||
|
|
861eab8050 | ||
|
|
b2c47a07a6 | ||
|
|
825b25a60b | ||
|
|
ece4041c74 | ||
|
|
88c8dbb7f3 | ||
|
|
42622f69d2 | ||
|
|
a8f0c7932f | ||
|
|
5c2b348c27 | ||
|
|
c8247a8ee0 | ||
|
|
6ced93722c | ||
|
|
5cfd951f28 | ||
|
|
6acba7172c | ||
|
|
6e7ff512ad | ||
|
|
9ff39ee241 | ||
|
|
2b90e9b580 | ||
|
|
02c5128fae | ||
|
|
c82a527ae4 | ||
|
|
0c9a7670fc | ||
|
|
a214d41f0c | ||
|
|
443581d676 | ||
|
|
577a577350 | ||
|
|
cd8379407a | ||
|
|
4e58dc5a0b | ||
|
|
4381f20146 | ||
|
|
b3fe8f83b1 | ||
|
|
828b1e4fbe | ||
|
|
686088e08c | ||
|
|
f164f9dea2 | ||
|
|
10acf3c738 | ||
|
|
9ee8d28fe8 | ||
|
|
c52581e4e3 | ||
|
|
5b411b9f3d | ||
|
|
1063072757 | ||
|
|
93841120aa | ||
|
|
27a63a9025 | ||
|
|
d51908b48d | ||
|
|
e0c159cb71 | ||
|
|
ce9bdadb69 | ||
|
|
082e36a34c | ||
|
|
861ed28e45 | ||
|
|
8dade95c75 | ||
|
|
1ae8b3e324 | ||
|
|
21dd61c597 | ||
|
|
49eeab4848 | ||
|
|
f47e22fe27 | ||
|
|
809bf19eb4 | ||
|
|
a1a5e5e299 | ||
|
|
df2c788b0b | ||
|
|
693368b735 | ||
|
|
ded9149466 | ||
|
|
f40e588e7d | ||
|
|
fdbac4ebff | ||
|
|
456f40dab2 | ||
|
|
ca81165a1c | ||
|
|
f548367e86 | ||
|
|
0975bc5c5f | ||
|
|
2eb0b4c8a0 | ||
|
|
9036110a1c | ||
|
|
7f2ebf6129 | ||
|
|
4a647ac19b | ||
|
|
d1549bf096 | ||
|
|
a3d1814343 | ||
|
|
6c8d4c6ec1 | ||
|
|
63e8744e78 | ||
|
|
e39402ff70 | ||
|
|
7d49baee6b | ||
|
|
ceb10a2ffe | ||
|
|
7986737e0e | ||
|
|
f6debb1629 | ||
|
|
8259563c33 | ||
|
|
3580f7f640 | ||
|
|
b655b7fe2d | ||
|
|
b42c1a45cc | ||
|
|
86d48bc082 | ||
|
|
44817d6685 | ||
|
|
62c27118d6 | ||
|
|
016c9a2562 | ||
|
|
b835ded157 | ||
|
|
016d52fa1b | ||
|
|
2b8f7a95d2 | ||
|
|
300547e59e | ||
|
|
93bffa29cc | ||
|
|
2a910c165e | ||
|
|
6412cbaf1c | ||
|
|
d9304001fe | ||
|
|
7d7897c3f6 | ||
|
|
1ae55da3f9 | ||
|
|
113bb3bfb4 | ||
|
|
83ea20545d | ||
|
|
965f79f31a | ||
|
|
f1c0cc9deb | ||
|
|
f2535cd2b9 | ||
|
|
42e584a381 | ||
|
|
0e1f9edaab | ||
|
|
c3fe3bc03d | ||
|
|
67097725d7 | ||
|
|
61468a359d | ||
|
|
b70d17d844 | ||
|
|
35fae815cf | ||
|
|
8c087792bf | ||
|
|
8627c54be9 | ||
|
|
41647a2e6c | ||
|
|
a73a98ddac | ||
|
|
3e49932382 | ||
|
|
406091fdcb | ||
|
|
c266296c4f | ||
|
|
01470f5ce1 | ||
|
|
82cee64860 | ||
|
|
60dd711856 | ||
|
|
bcbc17d3fe | ||
|
|
547212cd9e | ||
|
|
dec15c0ce0 | ||
|
|
e96ee56aa1 | ||
|
|
6e1f4eeddd | ||
|
|
70010ce8ee | ||
|
|
c1b62d8108 | ||
|
|
c690a1cb37 | ||
|
|
88d99cee43 | ||
|
|
b0c7b813e6 | ||
|
|
88f7f6a9d1 | ||
|
|
b9fac8ddb6 | ||
|
|
dacde7153f | ||
|
|
8f647d3489 | ||
|
|
e925f37b19 | ||
|
|
155a0af883 | ||
|
|
6fd360b594 | ||
|
|
e9aba02d5f | ||
|
|
4a8759f627 | ||
|
|
d00ec93133 | ||
|
|
e7e873c75c | ||
|
|
631c5b0c3b | ||
|
|
f024b0166f | ||
|
|
d6889a27b5 | ||
|
|
edbc72a7c9 | ||
|
|
a51f8c2a3c | ||
|
|
346b79ca22 | ||
|
|
2fd8e50f7f | ||
|
|
cecc5e0bab | ||
|
|
2325f9b042 | ||
|
|
4b29f7cbed | ||
|
|
7e82b42b29 | ||
|
|
f5c835b5d9 |
@@ -2,4 +2,6 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
.env
|
.env
|
||||||
agent/
|
agent/
|
||||||
.next
|
.next
|
||||||
|
docs/
|
||||||
|
screenshots/
|
||||||
5
.gitignore
vendored
@@ -1,5 +1,10 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
|
||||||
|
# Vitepress
|
||||||
|
docs/.vitepress/cache
|
||||||
|
docs/node_modules/
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
|||||||
21
Dockerfile
@@ -6,9 +6,13 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
COPY ./prisma ./prisma
|
COPY ./prisma ./prisma
|
||||||
|
|
||||||
|
# Install all dependencies (including devDependencies)
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
# Generate Prisma client
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
# Build the application
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
@@ -19,13 +23,16 @@ WORKDIR /app
|
|||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
|
||||||
# Install production dependencies INCLUDING prisma
|
# Copy package files
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm install --production --ignore-scripts
|
|
||||||
|
|
||||||
# Copy needed Prisma files
|
# Copy node_modules from builder
|
||||||
COPY --from=builder /app/node_modules/.prisma /app/node_modules/.prisma
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/node_modules/@prisma /app/node_modules/@prisma
|
|
||||||
|
# Remove dev dependencies
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# Copy Prisma files
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
|
||||||
# Copy built application
|
# Copy built application
|
||||||
@@ -36,5 +43,5 @@ COPY --from=builder /app/next.config.js* ./
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Run migrations first, then start app
|
# Run migrations and start
|
||||||
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
||||||
|
|||||||
35
README.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
The only dashboard you'll ever need to manage your entire server infrastructure. Keep all your server data organized in one central place, easily add your self-hosted applications with quick access links, and monitor their availability in real-time with built-in uptime tracking. Designed for simplicity and control, it gives you a clear overview of your entire self-hosted setup at a glance.
|
The only dashboard you'll ever need to manage your entire server infrastructure. Keep all your server data organized in one central place, easily add your self-hosted applications with quick access links, and monitor their availability in real-time with built-in uptime tracking. Designed for simplicity and control, it gives you a clear overview of your entire self-hosted setup at a glance.
|
||||||
|
|
||||||
<a href="https://buymeacoffee.com/corecontrol" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
[](https://www.buymeacoffee.com/corecontrol)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -17,31 +17,34 @@ The only dashboard you'll ever need to manage your entire server infrastructure.
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
Login Page:
|
Login Page:
|
||||||

|

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

|

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

|

|
||||||
|
|
||||||
|
Server Detail Page
|
||||||
|

|
||||||
|
|
||||||
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
|
- [X] 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)
|
||||||
|
|
||||||
@@ -57,14 +60,14 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
- agent
|
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
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
|
||||||
@@ -75,6 +78,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:
|
||||||
@@ -94,6 +102,7 @@ The application is build with:
|
|||||||
- Icons by [Lucide](https://lucide.dev/)
|
- Icons by [Lucide](https://lucide.dev/)
|
||||||
- Flowcharts by [React Flow](https://reactflow.dev/)
|
- Flowcharts by [React Flow](https://reactflow.dev/)
|
||||||
- Application icons by [selfh.st/icons](selfh.st/icons)
|
- Application icons by [selfh.st/icons](selfh.st/icons)
|
||||||
|
- Monitoring Tool by [Glances](https://github.com/nicolargo/glances)
|
||||||
- and a lot of love ❤️
|
- and a lot of love ❤️
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
745
agent/main.go
@@ -1,23 +1,95 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"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 Server struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Monitoring bool
|
||||||
|
MonitoringURL sql.NullString
|
||||||
|
Online bool
|
||||||
|
CpuUsage sql.NullFloat64
|
||||||
|
RamUsage sql.NullFloat64
|
||||||
|
DiskUsage sql.NullFloat64
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPUResponse struct {
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryResponse struct {
|
||||||
|
Active int64 `json:"active"`
|
||||||
|
Available int64 `json:"available"`
|
||||||
|
Buffers int64 `json:"buffers"`
|
||||||
|
Cached int64 `json:"cached"`
|
||||||
|
Free int64 `json:"free"`
|
||||||
|
Inactive int64 `json:"inactive"`
|
||||||
|
Percent float64 `json:"percent"`
|
||||||
|
Shared int64 `json:"shared"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Used int64 `json:"used"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FSResponse []struct {
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
MntPoint string `json:"mnt_point"`
|
||||||
|
Percent float64 `json:"percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID int
|
||||||
|
Enabled bool
|
||||||
|
Type string
|
||||||
|
SMTPHost sql.NullString
|
||||||
|
SMTPPort sql.NullInt64
|
||||||
|
SMTPFrom sql.NullString
|
||||||
|
SMTPUser sql.NullString
|
||||||
|
SMTPPass sql.NullString
|
||||||
|
SMTPSecure sql.NullBool
|
||||||
|
SMTPTo sql.NullString
|
||||||
|
TelegramChatID sql.NullString
|
||||||
|
TelegramToken sql.NullString
|
||||||
|
DiscordWebhook sql.NullString
|
||||||
|
GotifyUrl sql.NullString
|
||||||
|
GotifyToken sql.NullString
|
||||||
|
NtfyUrl sql.NullString
|
||||||
|
NtfyToken sql.NullString
|
||||||
|
PushoverUrl sql.NullString
|
||||||
|
PushoverToken sql.NullString
|
||||||
|
PushoverUser sql.NullString
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifications []Notification
|
||||||
|
notifMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
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 +106,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,43 +145,118 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
// Check for test notifications every 10 seconds
|
||||||
defer ticker.Stop()
|
go func() {
|
||||||
|
testNotifTicker := time.NewTicker(10 * time.Second)
|
||||||
|
defer testNotifTicker.Stop()
|
||||||
|
|
||||||
client := &http.Client{
|
for range testNotifTicker.C {
|
||||||
|
checkAndSendTestNotifications(db)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
appClient := &http.Client{
|
||||||
Timeout: 4 * time.Second,
|
Timeout: 4 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server monitoring every 5 seconds
|
||||||
|
go func() {
|
||||||
|
serverClient := &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
serverTicker := time.NewTicker(5 * time.Second)
|
||||||
|
defer serverTicker.Stop()
|
||||||
|
|
||||||
|
for range serverTicker.C {
|
||||||
|
servers := getServers(db)
|
||||||
|
checkAndUpdateServerStatus(db, serverClient, servers)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for now := range ticker.C {
|
for now := range ticker.C {
|
||||||
if now.Second()%10 != 0 {
|
if now.Second()%10 != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apps := getApplications(db)
|
apps := getApplications(db)
|
||||||
checkAndUpdateStatus(db, client, apps)
|
checkAndUpdateStatus(db, appClient, apps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper to safely copy slice
|
||||||
|
func notifMutexCopy(src []Notification) []Notification {
|
||||||
|
copyDst := make([]Notification, len(src))
|
||||||
|
copy(copyDst, src)
|
||||||
|
return copyDst
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPAddress(host string) bool {
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
return ip != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNotifications(db *sql.DB) ([]Notification, error) {
|
||||||
|
rows, err := db.Query(
|
||||||
|
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
|
||||||
|
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken"
|
||||||
|
FROM notification
|
||||||
|
WHERE enabled = true`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var configs []Notification
|
||||||
|
for rows.Next() {
|
||||||
|
var n Notification
|
||||||
|
if err := rows.Scan(
|
||||||
|
&n.ID, &n.Enabled, &n.Type,
|
||||||
|
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo,
|
||||||
|
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
|
||||||
|
); err != nil {
|
||||||
|
fmt.Printf("Error scanning notification: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configs = append(configs, n)
|
||||||
|
}
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func deleteOldEntries(db *sql.DB) error {
|
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()
|
||||||
|
|
||||||
|
// Delete old uptime history entries
|
||||||
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
|
||||||
}
|
}
|
||||||
affected, _ := res.RowsAffected()
|
affected, _ := res.RowsAffected()
|
||||||
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
|
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
|
||||||
|
|
||||||
|
// Delete old server history entries
|
||||||
|
res, err = db.ExecContext(ctx,
|
||||||
|
`DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, _ = res.RowsAffected()
|
||||||
|
fmt.Printf("Deleted %d old entries from server_history\n", affected)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +266,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
|
||||||
}
|
}
|
||||||
@@ -101,39 +275,546 @@ func getApplications(db *sql.DB) []Application {
|
|||||||
return apps
|
return apps
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
func getServers(db *sql.DB) []Server {
|
||||||
for _, app := range apps {
|
rows, err := db.Query(
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
|
||||||
defer cancel()
|
FROM server WHERE monitoring = true`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching servers: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "HEAD", app.PublicURL, nil)
|
var servers []Server
|
||||||
|
for rows.Next() {
|
||||||
|
var server Server
|
||||||
|
if err := rows.Scan(
|
||||||
|
&server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL,
|
||||||
|
&server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage,
|
||||||
|
); err != nil {
|
||||||
|
fmt.Printf("Error scanning server row: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
||||||
|
var notificationTemplate string
|
||||||
|
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||||
|
if err != nil || notificationTemplate == "" {
|
||||||
|
notificationTemplate = "The application !name (!url) went !status!"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range apps {
|
||||||
|
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
|
||||||
|
fmt.Printf("%s Checking...\n", logPrefix)
|
||||||
|
|
||||||
|
parsedURL, parseErr := url.Parse(app.PublicURL)
|
||||||
|
if parseErr != nil {
|
||||||
|
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hostIsIP := isIPAddress(parsedURL.Hostname())
|
||||||
|
|
||||||
|
httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||||
|
req, err := http.NewRequestWithContext(httpCtx, "HEAD", app.PublicURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating request: %v\n", err)
|
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
||||||
|
httpCancel()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
isOnline := false
|
|
||||||
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if err != nil || (resp != nil && (resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented)) {
|
||||||
isOnline = true
|
if resp != nil && resp.Body != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
fmt.Printf("%s HEAD failed, trying GET...\n", logPrefix)
|
||||||
|
|
||||||
|
req.Method = "GET"
|
||||||
|
resp, err = client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.ExecContext(ctx,
|
var isOnline bool
|
||||||
|
if err == nil && resp != nil {
|
||||||
|
isOnline = (resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 405
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
_, 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,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Update failed for app %d: %v\n", app.ID, err)
|
fmt.Printf("%s DB update failed: %v\n", logPrefix, err)
|
||||||
|
}
|
||||||
|
dbCancel()
|
||||||
|
|
||||||
|
dbCtx2, dbCancel2 := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_, err = db.ExecContext(dbCtx2,
|
||||||
|
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
||||||
|
app.ID, isOnline,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s Insert into history failed: %v\n", logPrefix, err)
|
||||||
|
}
|
||||||
|
dbCancel2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndUpdateServerStatus(db *sql.DB, client *http.Client, servers []Server) {
|
||||||
|
var notificationTemplate string
|
||||||
|
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||||
|
if err != nil || notificationTemplate == "" {
|
||||||
|
notificationTemplate = "The server !name is now !status!"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Monitoring || !server.MonitoringURL.Valid {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.ExecContext(ctx,
|
logPrefix := fmt.Sprintf("[Server %s]", server.Name)
|
||||||
`INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
fmt.Printf("%s Checking...\n", logPrefix)
|
||||||
app.ID,
|
|
||||||
isOnline,
|
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
|
||||||
)
|
var cpuUsage, ramUsage, diskUsage float64
|
||||||
|
var online = true
|
||||||
|
|
||||||
|
// Get CPU usage
|
||||||
|
cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Insert into uptime_history failed for app %d: %v\n", app.ID, err)
|
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
defer cpuResp.Body.Close()
|
||||||
|
|
||||||
|
if cpuResp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
var cpuData CPUResponse
|
||||||
|
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
|
||||||
|
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
cpuUsage = cpuData.Total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if online {
|
||||||
|
// Get Memory usage
|
||||||
|
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
defer memResp.Body.Close()
|
||||||
|
|
||||||
|
if memResp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
var memData MemoryResponse
|
||||||
|
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
|
||||||
|
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
// Calculate actual RAM usage excluding swap, cache, and buffers
|
||||||
|
// Formula: (total - free - cached - buffers) / total * 100
|
||||||
|
// This is the most accurate representation of actual used RAM
|
||||||
|
actualUsedRam := memData.Total - memData.Free - memData.Cached - memData.Buffers
|
||||||
|
if actualUsedRam < 0 {
|
||||||
|
actualUsedRam = 0 // Safeguard against negative values
|
||||||
|
}
|
||||||
|
|
||||||
|
if memData.Total > 0 {
|
||||||
|
ramUsage = float64(actualUsedRam) / float64(memData.Total) * 100
|
||||||
|
fmt.Printf("%s Calculated RAM usage: %.2f%% (Used: %d MB, Total: %d MB)\n",
|
||||||
|
logPrefix, ramUsage, actualUsedRam/1024/1024, memData.Total/1024/1024)
|
||||||
|
} else {
|
||||||
|
// Fallback to the provided percentage if calculation fails
|
||||||
|
ramUsage = memData.Percent
|
||||||
|
fmt.Printf("%s Using provided memory percentage because total is zero\n", logPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if online {
|
||||||
|
// Get Disk usage
|
||||||
|
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
defer fsResp.Body.Close()
|
||||||
|
|
||||||
|
if fsResp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else {
|
||||||
|
var fsData FSResponse
|
||||||
|
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
|
||||||
|
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
|
||||||
|
updateServerStatus(db, server.ID, false, 0, 0, 0)
|
||||||
|
online = false
|
||||||
|
} else if len(fsData) > 0 {
|
||||||
|
diskUsage = fsData[0].Percent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if status changed and send notification if needed
|
||||||
|
if online != server.Online {
|
||||||
|
status := "offline"
|
||||||
|
if online {
|
||||||
|
status = "online"
|
||||||
|
}
|
||||||
|
|
||||||
|
message := notificationTemplate
|
||||||
|
message = strings.ReplaceAll(message, "!name", server.Name)
|
||||||
|
message = strings.ReplaceAll(message, "!status", status)
|
||||||
|
|
||||||
|
sendNotifications(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server status with metrics
|
||||||
|
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
|
||||||
|
|
||||||
|
// Add entry to server history
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_, err = db.ExecContext(ctx,
|
||||||
|
`INSERT INTO server_history(
|
||||||
|
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt"
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, now())`,
|
||||||
|
server.ID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage),
|
||||||
|
)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s Failed to insert history: %v\n", logPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%\n",
|
||||||
|
logPrefix, cpuUsage, ramUsage, diskUsage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := db.ExecContext(ctx,
|
||||||
|
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8
|
||||||
|
WHERE id = $5`,
|
||||||
|
online, cpuUsage, ramUsage, diskUsage, serverID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendNotifications(message string) {
|
||||||
|
notifMutex.RLock()
|
||||||
|
notifs := notifMutexCopy(notifications)
|
||||||
|
notifMutex.RUnlock()
|
||||||
|
|
||||||
|
for _, n := range notifs {
|
||||||
|
switch n.Type {
|
||||||
|
case "email":
|
||||||
|
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||||
|
sendEmail(n, message)
|
||||||
|
}
|
||||||
|
case "telegram":
|
||||||
|
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||||
|
sendTelegram(n, message)
|
||||||
|
}
|
||||||
|
case "discord":
|
||||||
|
if n.DiscordWebhook.Valid {
|
||||||
|
sendDiscord(n, message)
|
||||||
|
}
|
||||||
|
case "gotify":
|
||||||
|
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
|
||||||
|
sendGotify(n, message)
|
||||||
|
}
|
||||||
|
case "ntfy":
|
||||||
|
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
|
||||||
|
sendNtfy(n, message)
|
||||||
|
}
|
||||||
|
case "pushover":
|
||||||
|
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
|
||||||
|
sendPushover(n, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEmail(n Notification, body string) {
|
||||||
|
// Initialize SMTP dialer with host, port, user, pass
|
||||||
|
d := gomail.NewDialer(
|
||||||
|
n.SMTPHost.String,
|
||||||
|
int(n.SMTPPort.Int64),
|
||||||
|
n.SMTPUser.String,
|
||||||
|
n.SMTPPass.String,
|
||||||
|
)
|
||||||
|
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
|
||||||
|
d.SSL = true
|
||||||
|
}
|
||||||
|
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", n.SMTPFrom.String)
|
||||||
|
m.SetHeader("To", n.SMTPTo.String)
|
||||||
|
m.SetHeader("Subject", "Uptime Notification")
|
||||||
|
m.SetBody("text/plain", body)
|
||||||
|
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
fmt.Printf("Email send failed: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTelegram(n Notification, message string) {
|
||||||
|
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
|
||||||
|
n.TelegramToken.String,
|
||||||
|
n.TelegramChatID.String,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Telegram send failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendDiscord(n Notification, message string) {
|
||||||
|
payload := fmt.Sprintf(`{"content": "%s"}`, message)
|
||||||
|
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Discord request creation failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Discord send failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendGotify(n Notification, message string) {
|
||||||
|
baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/")
|
||||||
|
targetURL := fmt.Sprintf("%s/message", baseURL)
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("message", message)
|
||||||
|
form.Add("priority", "5")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Gotify: ERROR creating request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-Gotify-Key", n.GotifyToken.String)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Gotify: ERROR sending request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendNtfy(n Notification, message string) {
|
||||||
|
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
|
||||||
|
topic := "corecontrol"
|
||||||
|
requestURL := fmt.Sprintf("%s/%s", baseURL, topic)
|
||||||
|
|
||||||
|
payload := map[string]string{"message": message}
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ntfy: ERROR marshaling JSON: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ntfy: ERROR creating request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.NtfyToken.Valid {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ntfy: ERROR sending request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPushover(n Notification, message string) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("token", n.PushoverToken.String)
|
||||||
|
form.Add("user", n.PushoverUser.String)
|
||||||
|
form.Add("message", message)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Pushover: ERROR creating request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Pushover: ERROR sending request: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndSendTestNotifications(db *sql.DB) {
|
||||||
|
// Query for test notifications
|
||||||
|
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching test notifications: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Process each test notification
|
||||||
|
var testIds []int
|
||||||
|
for rows.Next() {
|
||||||
|
var id, notificationId int
|
||||||
|
if err := rows.Scan(&id, ¬ificationId); err != nil {
|
||||||
|
fmt.Printf("Error scanning test notification: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to list of IDs to delete
|
||||||
|
testIds = append(testIds, id)
|
||||||
|
|
||||||
|
// Find the notification configuration
|
||||||
|
notifMutex.RLock()
|
||||||
|
for _, n := range notifications {
|
||||||
|
if n.ID == notificationId {
|
||||||
|
// Send test notification
|
||||||
|
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
|
||||||
|
sendSpecificNotification(n, "Test notification from CoreControl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifMutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete processed test notifications
|
||||||
|
if len(testIds) > 0 {
|
||||||
|
for _, id := range testIds {
|
||||||
|
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendSpecificNotification(n Notification, message string) {
|
||||||
|
switch n.Type {
|
||||||
|
case "email":
|
||||||
|
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||||
|
sendEmail(n, message)
|
||||||
|
}
|
||||||
|
case "telegram":
|
||||||
|
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||||
|
sendTelegram(n, message)
|
||||||
|
}
|
||||||
|
case "discord":
|
||||||
|
if n.DiscordWebhook.Valid {
|
||||||
|
sendDiscord(n, message)
|
||||||
|
}
|
||||||
|
case "gotify":
|
||||||
|
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
|
||||||
|
sendGotify(n, message)
|
||||||
|
}
|
||||||
|
case "ntfy":
|
||||||
|
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
|
||||||
|
sendNtfy(n, message)
|
||||||
|
}
|
||||||
|
case "pushover":
|
||||||
|
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
|
||||||
|
sendPushover(n, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,27 @@ const getTimeRange = (timespan: number) => {
|
|||||||
switch (timespan) {
|
switch (timespan) {
|
||||||
case 1:
|
case 1:
|
||||||
return {
|
return {
|
||||||
start: new Date(now.getTime() - 30 * 60 * 1000),
|
start: new Date(now.getTime() - 60 * 60 * 1000),
|
||||||
interval: 'minute'
|
interval: 'minute'
|
||||||
};
|
};
|
||||||
case 2:
|
case 2:
|
||||||
return {
|
return {
|
||||||
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
|
start: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||||
interval: '3hour'
|
interval: 'hour'
|
||||||
};
|
};
|
||||||
case 3:
|
case 3:
|
||||||
|
return {
|
||||||
|
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: 'day'
|
||||||
|
};
|
||||||
|
case 4:
|
||||||
return {
|
return {
|
||||||
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
|
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
|
||||||
interval: 'day'
|
interval: 'day'
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
start: new Date(now.getTime() - 30 * 60 * 1000),
|
start: new Date(now.getTime() - 60 * 60 * 1000),
|
||||||
interval: 'minute'
|
interval: 'minute'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -38,23 +43,31 @@ const generateIntervals = (timespan: number) => {
|
|||||||
now.setSeconds(0, 0);
|
now.setSeconds(0, 0);
|
||||||
|
|
||||||
switch (timespan) {
|
switch (timespan) {
|
||||||
case 1:
|
case 1: // 1 hour - 60 one-minute intervals
|
||||||
return Array.from({ length: 30 }, (_, i) => {
|
return Array.from({ length: 60 }, (_, i) => {
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
d.setMinutes(d.getMinutes() - i);
|
d.setMinutes(d.getMinutes() - i);
|
||||||
d.setSeconds(0, 0);
|
d.setSeconds(0, 0);
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
|
|
||||||
case 2:
|
case 2: // 1 day - 24 one-hour intervals
|
||||||
return Array.from({ length: 56 }, (_, i) => {
|
return Array.from({ length: 24 }, (_, i) => {
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
d.setHours(d.getHours() - (i * 3));
|
d.setHours(d.getHours() - i);
|
||||||
d.setMinutes(0, 0, 0);
|
d.setMinutes(0, 0, 0);
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
|
|
||||||
case 3:
|
case 3: // 7 days
|
||||||
|
return Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const d = new Date(now);
|
||||||
|
d.setDate(d.getDate() - i);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
|
||||||
|
case 4: // 30 days
|
||||||
return Array.from({ length: 30 }, (_, i) => {
|
return Array.from({ length: 30 }, (_, i) => {
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
d.setDate(d.getDate() - i);
|
d.setDate(d.getDate() - i);
|
||||||
@@ -70,14 +83,14 @@ const generateIntervals = (timespan: number) => {
|
|||||||
const getIntervalKey = (date: Date, timespan: number) => {
|
const getIntervalKey = (date: Date, timespan: number) => {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
switch (timespan) {
|
switch (timespan) {
|
||||||
case 1:
|
case 1: // 1 hour - minute intervals
|
||||||
d.setSeconds(0, 0);
|
d.setSeconds(0, 0);
|
||||||
return d.toISOString();
|
return d.toISOString();
|
||||||
case 2:
|
case 2: // 1 day - hour intervals
|
||||||
d.setHours(Math.floor(d.getHours() / 3) * 3);
|
|
||||||
d.setMinutes(0, 0, 0);
|
d.setMinutes(0, 0, 0);
|
||||||
return d.toISOString();
|
return d.toISOString();
|
||||||
case 3:
|
case 3: // 7 days - day intervals
|
||||||
|
case 4: // 30 days - day intervals
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
return d.toISOString();
|
return d.toISOString();
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -3,7 +3,19 @@ import { prisma } from "@/lib/prisma";
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const serverCount = await prisma.server.count();
|
const serverCountNoVMs = await prisma.server.count({
|
||||||
|
where: {
|
||||||
|
hostServer: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverCountOnlyVMs = await prisma.server.count({
|
||||||
|
where: {
|
||||||
|
hostServer: {
|
||||||
|
not: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const applicationCount = await prisma.application.count();
|
const applicationCount = await prisma.application.count();
|
||||||
|
|
||||||
@@ -12,7 +24,8 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
serverCount,
|
serverCountNoVMs,
|
||||||
|
serverCountOnlyVMs,
|
||||||
applicationCount,
|
applicationCount,
|
||||||
onlineApplicationsCount
|
onlineApplicationsCount
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ interface Server {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
host: boolean;
|
||||||
|
hostServer: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Application {
|
interface Application {
|
||||||
@@ -43,11 +45,15 @@ const NODE_WIDTH = 220;
|
|||||||
const NODE_HEIGHT = 60;
|
const NODE_HEIGHT = 60;
|
||||||
const APP_NODE_WIDTH = 160;
|
const APP_NODE_WIDTH = 160;
|
||||||
const APP_NODE_HEIGHT = 40;
|
const APP_NODE_HEIGHT = 40;
|
||||||
const HORIZONTAL_SPACING = 280;
|
const HORIZONTAL_SPACING = 700;
|
||||||
const VERTICAL_SPACING = 60;
|
const VERTICAL_SPACING = 80;
|
||||||
const START_Y = 120;
|
const START_Y = 120;
|
||||||
const ROOT_NODE_WIDTH = 300;
|
const ROOT_NODE_WIDTH = 300;
|
||||||
const CONTAINER_PADDING = 40;
|
const CONTAINER_PADDING = 40;
|
||||||
|
const COLUMN_SPACING = 220;
|
||||||
|
const VM_APP_SPACING = 220;
|
||||||
|
const MIN_VM_SPACING = 10;
|
||||||
|
const APP_ROW_SPACING = 15;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -60,74 +66,138 @@ export async function GET() {
|
|||||||
}) as Promise<Application[]>,
|
}) as Promise<Application[]>,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Root Node
|
// Level 2: Physical Servers
|
||||||
const rootNode: Node = {
|
const serverNodes: Node[] = servers
|
||||||
id: "root",
|
.filter(server => !server.hostServer)
|
||||||
type: "infrastructure",
|
.map((server, index, filteredServers) => {
|
||||||
data: { label: "My Infrastructure" },
|
const xPos =
|
||||||
position: { x: 0, y: 0 },
|
index * HORIZONTAL_SPACING -
|
||||||
style: {
|
((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2;
|
||||||
background: "#ffffff",
|
|
||||||
color: "#0f0f0f",
|
|
||||||
border: "2px solid #e6e4e1",
|
|
||||||
borderRadius: "8px",
|
|
||||||
padding: "16px",
|
|
||||||
width: ROOT_NODE_WIDTH,
|
|
||||||
height: NODE_HEIGHT,
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Server Nodes
|
return {
|
||||||
const serverNodes: Node[] = servers.map((server, index) => {
|
id: `server-${server.id}`,
|
||||||
const xPos =
|
type: "server",
|
||||||
index * HORIZONTAL_SPACING -
|
data: {
|
||||||
((servers.length - 1) * HORIZONTAL_SPACING) / 2;
|
label: `${server.name}\n${server.ip}`,
|
||||||
|
...server,
|
||||||
|
},
|
||||||
|
position: { x: xPos, y: START_Y },
|
||||||
|
style: {
|
||||||
|
background: "#ffffff",
|
||||||
|
color: "#0f0f0f",
|
||||||
|
border: "2px solid #e6e4e1",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "8px",
|
||||||
|
width: NODE_WIDTH,
|
||||||
|
height: NODE_HEIGHT,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
// Level 3: Services and VMs
|
||||||
id: `server-${server.id}`,
|
const serviceNodes: Node[] = [];
|
||||||
type: "server",
|
const vmNodes: Node[] = [];
|
||||||
data: {
|
|
||||||
label: `${server.name}\n${server.ip}`,
|
|
||||||
...server,
|
|
||||||
},
|
|
||||||
position: { x: xPos, y: START_Y },
|
|
||||||
style: {
|
|
||||||
background: "#ffffff",
|
|
||||||
color: "#0f0f0f",
|
|
||||||
border: "2px solid #e6e4e1",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: "8px",
|
|
||||||
width: NODE_WIDTH,
|
|
||||||
height: NODE_HEIGHT,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
lineHeight: "1.2",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Application Nodes
|
|
||||||
const appNodes: Node[] = [];
|
|
||||||
servers.forEach((server) => {
|
servers.forEach((server) => {
|
||||||
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
|
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
|
||||||
const serverX = serverNode?.position.x || 0;
|
if (serverNode) {
|
||||||
const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2;
|
const serverX = serverNode.position.x;
|
||||||
|
|
||||||
|
// Services (left column)
|
||||||
|
applications
|
||||||
|
.filter(app => app.serverId === server.id)
|
||||||
|
.forEach((app, appIndex) => {
|
||||||
|
serviceNodes.push({
|
||||||
|
id: `service-${app.id}`,
|
||||||
|
type: "service",
|
||||||
|
data: {
|
||||||
|
label: `${app.name}\n${app.localURL}`,
|
||||||
|
...app,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: serverX - COLUMN_SPACING,
|
||||||
|
y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20),
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
background: "#f0f9ff",
|
||||||
|
color: "#0f0f0f",
|
||||||
|
border: "2px solid #60a5fa",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "6px",
|
||||||
|
width: APP_NODE_WIDTH,
|
||||||
|
height: APP_NODE_HEIGHT,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
lineHeight: "1.1",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// VMs (middle column) mit dynamischem Abstand
|
||||||
|
const hostVMs = servers.filter(vm => vm.hostServer === server.id);
|
||||||
|
let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING;
|
||||||
|
|
||||||
|
hostVMs.forEach(vm => {
|
||||||
|
const appCount = applications.filter(app => app.serverId === vm.id).length;
|
||||||
|
|
||||||
|
vmNodes.push({
|
||||||
|
id: `vm-${vm.id}`,
|
||||||
|
type: "vm",
|
||||||
|
data: {
|
||||||
|
label: `${vm.name}\n${vm.ip}`,
|
||||||
|
...vm,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: serverX,
|
||||||
|
y: currentY,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
background: "#fef2f2",
|
||||||
|
color: "#0f0f0f",
|
||||||
|
border: "2px solid #fecaca",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "6px",
|
||||||
|
width: APP_NODE_WIDTH,
|
||||||
|
height: APP_NODE_HEIGHT,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
lineHeight: "1.1",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamischer Abstand basierend auf Anzahl Apps
|
||||||
|
const requiredSpace = appCount > 0
|
||||||
|
? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
currentY += Math.max(
|
||||||
|
requiredSpace + MIN_VM_SPACING,
|
||||||
|
MIN_VM_SPACING + APP_NODE_HEIGHT
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Level 4: VM Applications (right column)
|
||||||
|
const vmAppNodes: Node[] = [];
|
||||||
|
vmNodes.forEach((vm) => {
|
||||||
|
const vmX = vm.position.x;
|
||||||
applications
|
applications
|
||||||
.filter((app) => app.serverId === server.id)
|
.filter(app => app.serverId === vm.data.id)
|
||||||
.forEach((app, appIndex) => {
|
.forEach((app, appIndex) => {
|
||||||
appNodes.push({
|
vmAppNodes.push({
|
||||||
id: `app-${app.id}`,
|
id: `vm-app-${app.id}`,
|
||||||
type: "application",
|
type: "application",
|
||||||
data: {
|
data: {
|
||||||
label: `${app.name}\n${app.localURL}`,
|
label: `${app.name}\n${app.localURL}`,
|
||||||
...app,
|
...app,
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
x: serverX + xOffset,
|
x: vmX + VM_APP_SPACING,
|
||||||
y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING,
|
y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20),
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
@@ -145,38 +215,14 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connections
|
// Calculate dimensions for root node positioning
|
||||||
const connections: Edge[] = [
|
const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes];
|
||||||
...servers.map((server) => ({
|
|
||||||
id: `conn-root-${server.id}`,
|
|
||||||
source: "root",
|
|
||||||
target: `server-${server.id}`,
|
|
||||||
type: "straight",
|
|
||||||
style: {
|
|
||||||
stroke: "#94a3b8",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
...applications.map((app) => ({
|
|
||||||
id: `conn-${app.serverId}-${app.id}`,
|
|
||||||
source: `server-${app.serverId}`,
|
|
||||||
target: `app-${app.id}`,
|
|
||||||
type: "straight",
|
|
||||||
style: {
|
|
||||||
stroke: "#60a5fa",
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Container Box
|
|
||||||
const allNodes = [rootNode, ...serverNodes, ...appNodes];
|
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
let maxX = -Infinity;
|
let maxX = -Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxY = -Infinity;
|
let maxY = -Infinity;
|
||||||
|
|
||||||
allNodes.forEach((node) => {
|
tempNodes.forEach((node) => {
|
||||||
const width = parseInt(node.style.width?.toString() || "0", 10);
|
const width = parseInt(node.style.width?.toString() || "0", 10);
|
||||||
const height = parseInt(node.style.height?.toString() || "0", 10);
|
const height = parseInt(node.style.height?.toString() || "0", 10);
|
||||||
|
|
||||||
@@ -186,17 +232,47 @@ export async function GET() {
|
|||||||
maxY = Math.max(maxY, node.position.y + height);
|
maxY = Math.max(maxY, node.position.y + height);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const centerX = (minX + maxX) / 2;
|
||||||
|
const rootX = centerX - ROOT_NODE_WIDTH / 2;
|
||||||
|
|
||||||
|
// Level 1: Root Node (centered at top)
|
||||||
|
const rootNode: Node = {
|
||||||
|
id: "root",
|
||||||
|
type: "infrastructure",
|
||||||
|
data: { label: "My Infrastructure" },
|
||||||
|
position: { x: rootX, y: 0 },
|
||||||
|
style: {
|
||||||
|
background: "#ffffff",
|
||||||
|
color: "#0f0f0f",
|
||||||
|
border: "2px solid #e6e4e1",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "16px",
|
||||||
|
width: ROOT_NODE_WIDTH,
|
||||||
|
height: NODE_HEIGHT,
|
||||||
|
fontSize: "1.2rem",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update dimensions with root node
|
||||||
|
const allNodes = [rootNode, ...tempNodes];
|
||||||
|
let newMinX = Math.min(minX, rootNode.position.x);
|
||||||
|
let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH);
|
||||||
|
let newMinY = Math.min(minY, rootNode.position.y);
|
||||||
|
let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT);
|
||||||
|
|
||||||
|
// Container Node
|
||||||
const containerNode: Node = {
|
const containerNode: Node = {
|
||||||
id: 'container',
|
id: 'container',
|
||||||
type: 'container',
|
type: 'container',
|
||||||
data: { label: '' },
|
data: { label: '' },
|
||||||
position: {
|
position: {
|
||||||
x: minX - CONTAINER_PADDING,
|
x: newMinX - CONTAINER_PADDING,
|
||||||
y: minY - CONTAINER_PADDING
|
y: newMinY - CONTAINER_PADDING
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
width: maxX - minX + 2 * CONTAINER_PADDING,
|
width: newMaxX - newMinX + 2 * CONTAINER_PADDING,
|
||||||
height: maxY - minY + 2 * CONTAINER_PADDING,
|
height: newMaxY - newMinY + 2 * CONTAINER_PADDING,
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: '2px dashed #e2e8f0',
|
border: '2px dashed #e2e8f0',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
@@ -207,6 +283,116 @@ export async function GET() {
|
|||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Connections with hierarchical chaining
|
||||||
|
const connections: Edge[] = [];
|
||||||
|
|
||||||
|
// Root to Servers
|
||||||
|
serverNodes.forEach((server) => {
|
||||||
|
connections.push({
|
||||||
|
id: `conn-root-${server.id}`,
|
||||||
|
source: "root",
|
||||||
|
target: server.id,
|
||||||
|
type: "straight",
|
||||||
|
style: {
|
||||||
|
stroke: "#94a3b8",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Services chaining
|
||||||
|
const servicesByServer = new Map<number, Node[]>();
|
||||||
|
serviceNodes.forEach(service => {
|
||||||
|
const serverId = service.data.serverId;
|
||||||
|
if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []);
|
||||||
|
servicesByServer.get(serverId)!.push(service);
|
||||||
|
});
|
||||||
|
servicesByServer.forEach((services, serverId) => {
|
||||||
|
services.sort((a, b) => a.position.y - b.position.y);
|
||||||
|
services.forEach((service, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
connections.push({
|
||||||
|
id: `conn-service-${service.id}`,
|
||||||
|
source: `server-${serverId}`,
|
||||||
|
target: service.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#60a5fa", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const prevService = services[index - 1];
|
||||||
|
connections.push({
|
||||||
|
id: `conn-service-${service.id}-${prevService.id}`,
|
||||||
|
source: prevService.id,
|
||||||
|
target: service.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#60a5fa", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// VMs chaining
|
||||||
|
const vmsByHost = new Map<number, Node[]>();
|
||||||
|
vmNodes.forEach(vm => {
|
||||||
|
const hostId = vm.data.hostServer;
|
||||||
|
if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []);
|
||||||
|
vmsByHost.get(hostId)!.push(vm);
|
||||||
|
});
|
||||||
|
vmsByHost.forEach((vms, hostId) => {
|
||||||
|
vms.sort((a, b) => a.position.y - b.position.y);
|
||||||
|
vms.forEach((vm, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
connections.push({
|
||||||
|
id: `conn-vm-${vm.id}`,
|
||||||
|
source: `server-${hostId}`,
|
||||||
|
target: vm.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const prevVm = vms[index - 1];
|
||||||
|
connections.push({
|
||||||
|
id: `conn-vm-${vm.id}-${prevVm.id}`,
|
||||||
|
source: prevVm.id,
|
||||||
|
target: vm.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// VM Applications chaining
|
||||||
|
const appsByVM = new Map<number, Node[]>();
|
||||||
|
vmAppNodes.forEach(app => {
|
||||||
|
const vmId = app.data.serverId;
|
||||||
|
if (!appsByVM.has(vmId)) appsByVM.set(vmId, []);
|
||||||
|
appsByVM.get(vmId)!.push(app);
|
||||||
|
});
|
||||||
|
appsByVM.forEach((apps, vmId) => {
|
||||||
|
apps.sort((a, b) => a.position.y - b.position.y);
|
||||||
|
apps.forEach((app, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
connections.push({
|
||||||
|
id: `conn-vm-app-${app.id}`,
|
||||||
|
source: `vm-${vmId}`,
|
||||||
|
target: app.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const prevApp = apps[index - 1];
|
||||||
|
connections.push({
|
||||||
|
id: `conn-vm-app-${app.id}-${prevApp.id}`,
|
||||||
|
source: prevApp.id,
|
||||||
|
target: app.id,
|
||||||
|
type: "straight",
|
||||||
|
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
nodes: [containerNode, ...allNodes],
|
nodes: [containerNode, ...allNodes],
|
||||||
edges: connections,
|
edges: connections,
|
||||||
|
|||||||
57
app/api/notifications/add/route.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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;
|
||||||
|
gotifyUrl?: string;
|
||||||
|
gotifyToken?: string;
|
||||||
|
ntfyUrl?: string;
|
||||||
|
ntfyToken?: string;
|
||||||
|
pushoverUrl?: string;
|
||||||
|
pushoverToken?: string;
|
||||||
|
pushoverUser?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: AddRequest = await request.json();
|
||||||
|
const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser } = 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,
|
||||||
|
gotifyUrl: gotifyUrl,
|
||||||
|
gotifyToken: gotifyToken,
|
||||||
|
ntfyUrl: ntfyUrl,
|
||||||
|
ntfyToken: ntfyToken,
|
||||||
|
pushoverUrl: pushoverUrl,
|
||||||
|
pushoverToken: pushoverToken,
|
||||||
|
pushoverUser: pushoverUser,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
@@ -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
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/api/notifications/test/route.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface AddRequest {
|
||||||
|
notificationId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: AddRequest = await request.json();
|
||||||
|
const { notificationId } = body;
|
||||||
|
|
||||||
|
const notification = await prisma.test_notification.create({
|
||||||
|
data: {
|
||||||
|
notificationId: notificationId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Success", notification });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ 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;
|
||||||
|
icon: string;
|
||||||
os: string;
|
os: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,24 +13,31 @@ interface AddRequest {
|
|||||||
gpu: string;
|
gpu: string;
|
||||||
ram: string;
|
ram: string;
|
||||||
disk: string;
|
disk: string;
|
||||||
|
monitoring: boolean;
|
||||||
|
monitoringURL: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
|
||||||
|
|
||||||
const server = await prisma.server.create({
|
const server = await prisma.server.create({
|
||||||
data: {
|
data: {
|
||||||
|
host,
|
||||||
|
hostServer,
|
||||||
name,
|
name,
|
||||||
|
icon,
|
||||||
os,
|
os,
|
||||||
ip,
|
ip,
|
||||||
url,
|
url,
|
||||||
cpu,
|
cpu,
|
||||||
gpu,
|
gpu,
|
||||||
ram,
|
ram,
|
||||||
disk
|
disk,
|
||||||
|
monitoring,
|
||||||
|
monitoringURL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
|
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete all server history records for this server
|
||||||
|
await prisma.server_history.deleteMany({
|
||||||
|
where: { serverId: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the server
|
||||||
await prisma.server.delete({
|
await prisma.server.delete({
|
||||||
where: { id: id }
|
where: { id: id }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ 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;
|
||||||
|
icon: string;
|
||||||
os: string;
|
os: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -11,29 +14,43 @@ interface EditRequest {
|
|||||||
gpu: string;
|
gpu: string;
|
||||||
ram: string;
|
ram: string;
|
||||||
disk: string;
|
disk: string;
|
||||||
|
monitoring: boolean;
|
||||||
|
monitoringURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
|
||||||
|
|
||||||
const existingServer = await prisma.server.findUnique({ where: { id } });
|
const existingServer = await prisma.server.findUnique({ where: { id } });
|
||||||
if (!existingServer) {
|
if (!existingServer) {
|
||||||
return NextResponse.json({ error: "Server not found" }, { status: 404 });
|
return NextResponse.json({ error: "Server not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newHostServer = hostServer;
|
||||||
|
if (hostServer === null) {
|
||||||
|
newHostServer = 0;
|
||||||
|
} else {
|
||||||
|
newHostServer = hostServer;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedServer = await prisma.server.update({
|
const updatedServer = await prisma.server.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
host,
|
||||||
|
hostServer: newHostServer,
|
||||||
name,
|
name,
|
||||||
|
icon,
|
||||||
os,
|
os,
|
||||||
ip,
|
ip,
|
||||||
url,
|
url,
|
||||||
cpu,
|
cpu,
|
||||||
gpu,
|
gpu,
|
||||||
ram,
|
ram,
|
||||||
disk
|
disk,
|
||||||
|
monitoring,
|
||||||
|
monitoringURL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,247 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
interface GetRequest {
|
interface GetRequest {
|
||||||
page?: number;
|
page?: number;
|
||||||
ITEMS_PER_PAGE?: number;
|
ITEMS_PER_PAGE?: number;
|
||||||
|
timeRange?: '1h' | '1d' | '7d' | '30d';
|
||||||
|
serverId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTimeRange = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => {
|
||||||
|
const now = new Date();
|
||||||
|
switch (timeRange) {
|
||||||
|
case '1d':
|
||||||
|
return {
|
||||||
|
start: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||||
|
end: now,
|
||||||
|
intervalMinutes: 15 // 15 minute intervals
|
||||||
|
};
|
||||||
|
case '7d':
|
||||||
|
return {
|
||||||
|
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
|
||||||
|
end: now,
|
||||||
|
intervalMinutes: 60 // 1 hour intervals
|
||||||
|
};
|
||||||
|
case '30d':
|
||||||
|
return {
|
||||||
|
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
|
||||||
|
end: now,
|
||||||
|
intervalMinutes: 240 // 4 hour intervals
|
||||||
|
};
|
||||||
|
case '1h':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
start: new Date(now.getTime() - 60 * 60 * 1000),
|
||||||
|
end: now,
|
||||||
|
intervalMinutes: 1 // 1 minute intervals
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIntervals = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => {
|
||||||
|
const { start, end, intervalMinutes } = getTimeRange(timeRange);
|
||||||
|
|
||||||
|
let intervalCount: number;
|
||||||
|
switch (timeRange) {
|
||||||
|
case '1d':
|
||||||
|
intervalCount = 96; // 24 hours * 4 (15-minute intervals)
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
intervalCount = 168; // 7 days * 24 hours
|
||||||
|
break;
|
||||||
|
case '30d':
|
||||||
|
intervalCount = 180; // 30 days * 6 (4-hour intervals)
|
||||||
|
break;
|
||||||
|
case '1h':
|
||||||
|
default:
|
||||||
|
intervalCount = 60;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the total time span in minutes
|
||||||
|
const totalMinutes = Math.floor((end.getTime() - start.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
// Create equally spaced intervals
|
||||||
|
return Array.from({ length: intervalCount }, (_, i) => {
|
||||||
|
const minutesFromEnd = Math.floor(i * (totalMinutes / (intervalCount - 1)));
|
||||||
|
const d = new Date(end.getTime() - minutesFromEnd * 60 * 1000);
|
||||||
|
return d;
|
||||||
|
}).reverse(); // Return in chronological order
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseUsageValue = (value: string | null): number => {
|
||||||
|
if (!value) return 0;
|
||||||
|
return Math.round(parseFloat(value.replace('%', '')) * 100) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
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 timeRange = body.timeRange || '1h';
|
||||||
const servers = await prisma.server.findMany({
|
const serverId = body.serverId;
|
||||||
skip: (page - 1) * ITEMS_PER_PAGE,
|
|
||||||
take: ITEMS_PER_PAGE,
|
|
||||||
orderBy: { name: 'asc' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalCount = await prisma.server.count();
|
// If serverId is provided, only fetch that specific server
|
||||||
const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
|
const hostsQuery = serverId
|
||||||
|
? { id: serverId }
|
||||||
|
: { hostServer: 0 };
|
||||||
|
|
||||||
|
let hosts;
|
||||||
|
if (!serverId) {
|
||||||
|
hosts = await prisma.server.findMany({
|
||||||
|
where: hostsQuery,
|
||||||
|
orderBy: { name: 'asc' as Prisma.SortOrder },
|
||||||
|
skip: (page - 1) * ITEMS_PER_PAGE,
|
||||||
|
take: ITEMS_PER_PAGE,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hosts = await prisma.server.findMany({
|
||||||
|
where: hostsQuery,
|
||||||
|
orderBy: { name: 'asc' as Prisma.SortOrder },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start } = getTimeRange(timeRange);
|
||||||
|
const intervals = getIntervals(timeRange);
|
||||||
|
|
||||||
|
const hostsWithVms = await Promise.all(
|
||||||
|
hosts.map(async (host) => {
|
||||||
|
const vms = await prisma.server.findMany({
|
||||||
|
where: { hostServer: host.id },
|
||||||
|
orderBy: { name: 'asc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get server history for the host
|
||||||
|
const serverHistory = await prisma.server_history.findMany({
|
||||||
|
where: {
|
||||||
|
serverId: host.id,
|
||||||
|
createdAt: {
|
||||||
|
gte: start
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'asc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process history data into intervals
|
||||||
|
const historyMap = new Map<string, {
|
||||||
|
cpu: number[],
|
||||||
|
ram: number[],
|
||||||
|
disk: number[],
|
||||||
|
online: boolean[]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Initialize intervals
|
||||||
|
intervals.forEach(date => {
|
||||||
|
const key = date.toISOString();
|
||||||
|
historyMap.set(key, {
|
||||||
|
cpu: [],
|
||||||
|
ram: [],
|
||||||
|
disk: [],
|
||||||
|
online: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group data by interval
|
||||||
|
serverHistory.forEach(record => {
|
||||||
|
const recordDate = new Date(record.createdAt);
|
||||||
|
let nearestInterval: Date = intervals[0];
|
||||||
|
let minDiff = Infinity;
|
||||||
|
|
||||||
|
// Find the nearest interval for this record
|
||||||
|
intervals.forEach(intervalDate => {
|
||||||
|
const diff = Math.abs(recordDate.getTime() - intervalDate.getTime());
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff;
|
||||||
|
nearestInterval = intervalDate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = nearestInterval.toISOString();
|
||||||
|
const interval = historyMap.get(key);
|
||||||
|
if (interval) {
|
||||||
|
interval.cpu.push(parseUsageValue(record.cpuUsage));
|
||||||
|
interval.ram.push(parseUsageValue(record.ramUsage));
|
||||||
|
interval.disk.push(parseUsageValue(record.diskUsage));
|
||||||
|
interval.online.push(record.online);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate averages for each interval
|
||||||
|
const historyData = intervals.map(date => {
|
||||||
|
const key = date.toISOString();
|
||||||
|
const data = historyMap.get(key) || {
|
||||||
|
cpu: [],
|
||||||
|
ram: [],
|
||||||
|
disk: [],
|
||||||
|
online: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const average = (arr: number[]) =>
|
||||||
|
arr.length ? Math.round((arr.reduce((a, b) => a + b, 0) / arr.length) * 100) / 100 : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: key,
|
||||||
|
cpu: average(data.cpu),
|
||||||
|
ram: average(data.ram),
|
||||||
|
disk: average(data.disk),
|
||||||
|
online: data.online.length ?
|
||||||
|
data.online.filter(Boolean).length / data.online.length >= 0.5
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add isVM flag to VMs
|
||||||
|
const vmsWithFlag = vms.map(vm => ({
|
||||||
|
...vm,
|
||||||
|
isVM: true,
|
||||||
|
hostedVMs: [] // Initialize empty hostedVMs array for VMs
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...host,
|
||||||
|
isVM: false,
|
||||||
|
hostedVMs: vmsWithFlag,
|
||||||
|
history: {
|
||||||
|
labels: intervals.map(d => d.toISOString()),
|
||||||
|
datasets: {
|
||||||
|
cpu: intervals.map(d => {
|
||||||
|
const data = historyMap.get(d.toISOString())?.cpu || [];
|
||||||
|
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
|
||||||
|
}),
|
||||||
|
ram: intervals.map(d => {
|
||||||
|
const data = historyMap.get(d.toISOString())?.ram || [];
|
||||||
|
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
|
||||||
|
}),
|
||||||
|
disk: intervals.map(d => {
|
||||||
|
const data = historyMap.get(d.toISOString())?.disk || [];
|
||||||
|
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
|
||||||
|
}),
|
||||||
|
online: intervals.map(d => {
|
||||||
|
const data = historyMap.get(d.toISOString())?.online || [];
|
||||||
|
return data.length ? data.filter(Boolean).length / data.length >= 0.5 : null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only calculate maxPage when not requesting a specific server
|
||||||
|
let maxPage = 1;
|
||||||
|
if (!serverId) {
|
||||||
|
const totalHosts = await prisma.server.count({
|
||||||
|
where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
|
||||||
|
});
|
||||||
|
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
servers,
|
servers: hostsWithVms,
|
||||||
maxPage
|
maxPage
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
21
app/api/servers/hosts/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add required properties to ensure consistency
|
||||||
|
const serversWithProps = servers.map(server => ({
|
||||||
|
...server,
|
||||||
|
isVM: false,
|
||||||
|
hostedVMs: [] // Initialize empty hostedVMs array
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json({ servers: serversWithProps });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/api/servers/monitoring/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const servers = await prisma.server.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
online: true,
|
||||||
|
cpuUsage: true,
|
||||||
|
ramUsage: true,
|
||||||
|
diskUsage: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitoringData = servers.map((server: {
|
||||||
|
id: number;
|
||||||
|
online: boolean;
|
||||||
|
cpuUsage: string | null;
|
||||||
|
ramUsage: string | null;
|
||||||
|
diskUsage: string | null;
|
||||||
|
}) => ({
|
||||||
|
id: server.id,
|
||||||
|
online: server.online,
|
||||||
|
cpuUsage: server.cpuUsage ? parseInt(server.cpuUsage) : 0,
|
||||||
|
ramUsage: server.ramUsage ? parseInt(server.ramUsage) : 0,
|
||||||
|
diskUsage: server.diskUsage ? parseInt(server.diskUsage) : 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json(monitoringData)
|
||||||
|
} catch (error) {
|
||||||
|
return new NextResponse("Internal Error", { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,51 @@ export async function POST(request: NextRequest) {
|
|||||||
const body: SearchRequest = await request.json();
|
const body: SearchRequest = await request.json();
|
||||||
const { searchterm } = body;
|
const { searchterm } = body;
|
||||||
|
|
||||||
|
// Fetch all servers
|
||||||
const servers = await prisma.server.findMany({});
|
const servers = await prisma.server.findMany({});
|
||||||
|
|
||||||
|
// Create a map of host servers with their hosted VMs
|
||||||
|
const serverMap = new Map();
|
||||||
|
servers.forEach(server => {
|
||||||
|
if (server.host) {
|
||||||
|
serverMap.set(server.id, {
|
||||||
|
...server,
|
||||||
|
isVM: false,
|
||||||
|
hostedVMs: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add VMs to their host servers and mark them as VMs
|
||||||
|
const serversWithType = servers.map(server => {
|
||||||
|
// If not a host and has a hostServer, it's a VM
|
||||||
|
if (!server.host && server.hostServer) {
|
||||||
|
const hostServer = serverMap.get(server.hostServer);
|
||||||
|
if (hostServer) {
|
||||||
|
hostServer.hostedVMs.push({
|
||||||
|
...server,
|
||||||
|
isVM: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
isVM: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
isVM: false,
|
||||||
|
hostedVMs: serverMap.get(server.id)?.hostedVMs || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const fuseOptions = {
|
const fuseOptions = {
|
||||||
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'],
|
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'],
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuse = new Fuse(servers, fuseOptions);
|
const fuse = new Fuse(serversWithType, fuseOptions);
|
||||||
|
|
||||||
const searchResults = fuse.search(searchterm);
|
const searchResults = fuse.search(searchterm);
|
||||||
|
|
||||||
|
|||||||
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_application": "", "notification_text_server": "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If settings entry exists, fetch it
|
||||||
|
const settings = await prisma.settings.findFirst({
|
||||||
|
where: { id: existingSettings.id },
|
||||||
|
});
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" });
|
||||||
|
}
|
||||||
|
// Return the settings entry
|
||||||
|
return NextResponse.json({ "notification_text_application": settings.notification_text_application, "notification_text_server": settings.notification_text_server });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/api/settings/notification_text/route.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface AddRequest {
|
||||||
|
text_application: string;
|
||||||
|
text_server: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: AddRequest = await request.json();
|
||||||
|
const { text_application, text_server } = 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_application: text_application, notification_text_server: text_server },
|
||||||
|
});
|
||||||
|
return NextResponse.json({ message: "Success", updatedSettings });
|
||||||
|
}
|
||||||
|
// If no settings entry exists, create a new one
|
||||||
|
const settings = await prisma.settings.create({
|
||||||
|
data: {
|
||||||
|
notification_text_application: text_application,
|
||||||
|
notification_text_server: text_server,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Success", settings });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,20 +19,23 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
interface StatsResponse {
|
interface StatsResponse {
|
||||||
serverCount: number
|
serverCountNoVMs: number
|
||||||
|
serverCountOnlyVMs: number
|
||||||
applicationCount: number
|
applicationCount: number
|
||||||
onlineApplicationsCount: number
|
onlineApplicationsCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [serverCount, setServerCount] = useState<number>(0)
|
const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
|
||||||
|
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
|
||||||
const [applicationCount, setApplicationCount] = useState<number>(0)
|
const [applicationCount, setApplicationCount] = useState<number>(0)
|
||||||
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
|
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
|
||||||
|
|
||||||
const getStats = async () => {
|
const getStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
|
const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
|
||||||
setServerCount(response.data.serverCount)
|
setServerCountNoVMs(response.data.serverCountNoVMs)
|
||||||
|
setServerCountOnlyVMs(response.data.serverCountOnlyVMs)
|
||||||
setApplicationCount(response.data.applicationCount)
|
setApplicationCount(response.data.applicationCount)
|
||||||
setOnlineApplicationsCount(response.data.onlineApplicationsCount)
|
setOnlineApplicationsCount(response.data.onlineApplicationsCount)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -69,53 +72,94 @@ export default function Dashboard() {
|
|||||||
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
|
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
|
||||||
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-sm transition-all hover:shadow-md">
|
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-lg transition-all hover:shadow-xl hover:border-t-rose-600">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="py-3 pb-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl font-medium">Servers</CardTitle>
|
<div>
|
||||||
<Server className="h-6 w-6 text-rose-500" />
|
<CardTitle className="text-2xl font-semibold">Servers</CardTitle>
|
||||||
|
<CardDescription className="mt-1">Physical and virtual servers overview</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Manage your server infrastructure</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-2 pb-4">
|
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||||
<div className="text-4xl font-bold">{serverCount}</div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<p className="text-sm text-muted-foreground mt-2">Active servers</p>
|
{/* Physical Servers */}
|
||||||
|
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
|
||||||
|
<div className="bg-rose-100 p-2 rounded-full">
|
||||||
|
<Server className="h-6 w-6 text-rose-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-3xl font-bold">{serverCountNoVMs}</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Physical Servers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Virtual Machines */}
|
||||||
|
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
|
||||||
|
<div className="bg-violet-100 p-2 rounded-full">
|
||||||
|
<Network className="h-6 w-6 text-violet-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-3xl font-bold">{serverCountOnlyVMs}</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Virtual Servers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="border-t bg-muted/20 p-4">
|
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
<Button
|
||||||
<Link href="/dashboard/servers">View all servers</Link>
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/dashboard/servers" className="flex items-center justify-between">
|
||||||
|
<span>Manage Servers</span>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-sm transition-all hover:shadow-md">
|
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-lg transition-all hover:shadow-xl hover:border-t-amber-600">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="py-3 pb-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl font-medium">Applications</CardTitle>
|
<div>
|
||||||
<Layers className="h-6 w-6 text-amber-500" />
|
<CardTitle className="text-2xl font-semibold">Applications</CardTitle>
|
||||||
|
<CardDescription className="mt-1">Manage your deployed applications</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Layers className="h-8 w-8 text-amber-500 p-1.5 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Manage your deployed applications</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-2 pb-4">
|
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||||
<div className="text-4xl font-bold">{applicationCount}</div>
|
<div className="text-4xl font-bold">{applicationCount}</div>
|
||||||
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
|
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="border-t bg-muted/20 p-4">
|
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
<Button
|
||||||
<Link href="/dashboard/applications">View all applications</Link>
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/dashboard/applications" className="flex items-center justify-between">
|
||||||
|
<span>View all applications</span>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-sm transition-all hover:shadow-md">
|
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-lg transition-all hover:shadow-xl hover:border-t-emerald-600">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="py-3 pb-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl font-medium">Uptime</CardTitle>
|
<div>
|
||||||
<Activity className="h-6 w-6 text-emerald-500" />
|
<CardTitle className="text-2xl font-semibold">Uptime</CardTitle>
|
||||||
|
<CardDescription className="mt-1">Monitor your service availability</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Monitor your service availability</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-2 pb-4">
|
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-4xl font-bold flex items-center justify-between">
|
<div className="text-4xl font-bold flex items-center justify-between">
|
||||||
<span>
|
<span>
|
||||||
@@ -136,28 +180,44 @@ export default function Dashboard() {
|
|||||||
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
|
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="border-t bg-muted/20 p-4">
|
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
<Button
|
||||||
<Link href="/dashboard/uptime">View uptime metrics</Link>
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/dashboard/uptime" className="flex items-center justify-between">
|
||||||
|
<span>View uptime metrics</span>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-sm transition-all hover:shadow-md">
|
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-lg transition-all hover:shadow-xl hover:border-t-sky-600">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="py-3 pb-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-xl font-medium">Network</CardTitle>
|
<div>
|
||||||
<Network className="h-6 w-6 text-sky-500" />
|
<CardTitle className="text-2xl font-semibold">Network</CardTitle>
|
||||||
|
<CardDescription className="mt-1">Manage network configuration</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Network className="h-8 w-8 text-sky-500 p-1.5 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Manage network configuration</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-2 pb-4">
|
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||||
<div className="text-4xl font-bold">{serverCount + applicationCount}</div>
|
<div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div>
|
||||||
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
|
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="border-t bg-muted/20 p-4">
|
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||||
<Button variant="ghost" size="default" className="w-full hover:bg-background font-medium" asChild>
|
<Button
|
||||||
<Link href="/dashboard/network">View network details</Link>
|
variant="outline"
|
||||||
|
size="default"
|
||||||
|
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/dashboard/network" className="flex items-center justify-between">
|
||||||
|
<span>View network details</span>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
|
import { StatusIndicator } from "@/components/status-indicator";
|
||||||
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
interface Application {
|
interface Application {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -115,21 +118,19 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [maxPage, setMaxPage] = useState<number>(1);
|
const [maxPage, setMaxPage] = useState<number>(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
|
|
||||||
const [applications, setApplications] = useState<Application[]>([]);
|
const [applications, setApplications] = useState<Application[]>([]);
|
||||||
const [servers, setServers] = useState<Server[]>([]);
|
const [servers, setServers] = useState<Server[]>([]);
|
||||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [isSearching, setIsSearching] = useState<boolean>(false);
|
const [isSearching, setIsSearching] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const savedLayout = Cookies.get("layoutPreference-app");
|
||||||
const savedLayout = Cookies.get("layoutPreference-app");
|
const initialIsGridLayout = savedLayout === "grid";
|
||||||
const layout_bool = savedLayout === "grid";
|
const initialItemsPerPage = initialIsGridLayout ? 15 : 5;
|
||||||
setIsGridLayout(layout_bool);
|
|
||||||
setItemsPerPage(layout_bool ? 15 : 5);
|
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
|
||||||
}, []);
|
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||||
|
|
||||||
const toggleLayout = () => {
|
const toggleLayout = () => {
|
||||||
const newLayout = !isGridLayout;
|
const newLayout = !isGridLayout;
|
||||||
@@ -153,8 +154,10 @@ export default function Dashboard() {
|
|||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
getApplications();
|
getApplications();
|
||||||
|
toast.success("Application added successfully");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
|
toast.error("Failed to add application");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,6 +174,7 @@ export default function Dashboard() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
|
toast.error("Failed to get applications");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,8 +190,10 @@ export default function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
await axios.post("/api/applications/delete", { id });
|
await axios.post("/api/applications/delete", { id });
|
||||||
getApplications();
|
getApplications();
|
||||||
|
toast.success("Application deleted successfully");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
|
toast.error("Failed to delete application");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,8 +222,10 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
getApplications();
|
getApplications();
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
|
toast.success("Application edited successfully");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response.data);
|
console.log(error.response.data);
|
||||||
|
toast.error("Failed to edit application");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,6 +289,7 @@ export default function Dashboard() {
|
|||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<Toaster />
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-3xl font-bold">Your Applications</span>
|
<span className="text-3xl font-bold">Your Applications</span>
|
||||||
@@ -440,20 +449,9 @@ export default function Dashboard() {
|
|||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 right-2">
|
||||||
<div
|
<StatusIndicator isOnline={app.online} />
|
||||||
className={`w-4 h-4 rounded-full flex items-center justify-center ${
|
|
||||||
app.online ? "bg-green-700" : "bg-red-700"
|
|
||||||
}`}
|
|
||||||
title={app.online ? "Online" : "Offline"}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 rounded-full ${
|
|
||||||
app.online ? "bg-green-500" : "bg-red-500"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full mt-4 mb-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
|
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
|
||||||
{app.icon ? (
|
{app.icon ? (
|
||||||
@@ -479,7 +477,7 @@ export default function Dashboard() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end justify-start space-y-2 w-[270px]">
|
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<div className="flex flex-col space-y-2 flex-grow">
|
<div className="flex flex-col space-y-2 flex-grow">
|
||||||
<Button
|
<Button
|
||||||
@@ -490,7 +488,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
Open Public URL
|
Public URL
|
||||||
</Button>
|
</Button>
|
||||||
{app.localURL && (
|
{app.localURL && (
|
||||||
<Button
|
<Button
|
||||||
@@ -501,7 +499,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Home className="h-4 w-4" />
|
<Home className="h-4 w-4" />
|
||||||
Open Local URL
|
Local URL
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
735
app/dashboard/servers/[server_id]/Server.tsx
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useParams } from "next/navigation"
|
||||||
|
import axios from "axios"
|
||||||
|
import Chart from 'chart.js/auto'
|
||||||
|
import { AppSidebar } from "@/components/app-sidebar"
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb"
|
||||||
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Link, Cpu, MicroscopeIcon as Microchip, MemoryStick, HardDrive, MonitorIcon as MonitorCog, FileDigit, History } from "lucide-react"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { StatusIndicator } from "@/components/status-indicator"
|
||||||
|
import { DynamicIcon } from "lucide-react/dynamic"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import NextLink from "next/link"
|
||||||
|
|
||||||
|
interface ServerHistory {
|
||||||
|
labels: string[];
|
||||||
|
datasets: {
|
||||||
|
cpu: (number | null)[];
|
||||||
|
ram: (number | null)[];
|
||||||
|
disk: (number | null)[];
|
||||||
|
online: (boolean | null)[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
host: boolean;
|
||||||
|
hostServer: number | null;
|
||||||
|
os?: string;
|
||||||
|
ip?: string;
|
||||||
|
url?: string;
|
||||||
|
cpu?: string;
|
||||||
|
gpu?: string;
|
||||||
|
ram?: string;
|
||||||
|
disk?: string;
|
||||||
|
hostedVMs?: Server[];
|
||||||
|
isVM?: boolean;
|
||||||
|
monitoring?: boolean;
|
||||||
|
monitoringURL?: string;
|
||||||
|
online?: boolean;
|
||||||
|
cpuUsage: number;
|
||||||
|
ramUsage: number;
|
||||||
|
diskUsage: number;
|
||||||
|
history?: ServerHistory;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetServersResponse {
|
||||||
|
servers: Server[];
|
||||||
|
maxPage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerDetail() {
|
||||||
|
const params = useParams()
|
||||||
|
const serverId = params.server_id as string
|
||||||
|
const [server, setServer] = useState<Server | null>(null)
|
||||||
|
const [timeRange, setTimeRange] = useState<'1h' | '1d' | '7d' | '30d'>('1h')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
// Chart references
|
||||||
|
const cpuChartRef = { current: null as Chart | null }
|
||||||
|
const ramChartRef = { current: null as Chart | null }
|
||||||
|
const diskChartRef = { current: null as Chart | null }
|
||||||
|
|
||||||
|
const fetchServerDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const response = await axios.post<GetServersResponse>("/api/servers/get", {
|
||||||
|
serverId: parseInt(serverId),
|
||||||
|
timeRange: timeRange
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.servers && response.data.servers.length > 0) {
|
||||||
|
setServer(response.data.servers[0])
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch server details:", error)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServerDetails()
|
||||||
|
}, [serverId, timeRange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!server || !server.history) return;
|
||||||
|
|
||||||
|
// Clean up existing charts
|
||||||
|
if (cpuChartRef.current) cpuChartRef.current.destroy();
|
||||||
|
if (ramChartRef.current) ramChartRef.current.destroy();
|
||||||
|
if (diskChartRef.current) diskChartRef.current.destroy();
|
||||||
|
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
const initTimer = setTimeout(() => {
|
||||||
|
const history = server.history as ServerHistory;
|
||||||
|
|
||||||
|
// Format time labels based on the selected time range
|
||||||
|
const timeLabels = history.labels.map((date: string) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
if (timeRange === '1h') {
|
||||||
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
|
} else if (timeRange === '1d') {
|
||||||
|
// For 1 day, show hours and minutes
|
||||||
|
return d.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
} else if (timeRange === '7d') {
|
||||||
|
// For 7 days, show day and time
|
||||||
|
return d.toLocaleDateString([], {
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric'
|
||||||
|
}) + ' ' + d.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For 30 days
|
||||||
|
return d.toLocaleDateString([], {
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a time range title for the chart
|
||||||
|
const getRangeTitle = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const startDate = new Date(history.labels[0])
|
||||||
|
|
||||||
|
if (timeRange === '1h') {
|
||||||
|
return `Last Hour (${startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`
|
||||||
|
} else if (timeRange === '1d') {
|
||||||
|
return `Last 24 Hours (${startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`
|
||||||
|
} else if (timeRange === '7d') {
|
||||||
|
return `Last 7 Days (${startDate.toLocaleDateString([], { month: 'short', day: 'numeric' })} - ${now.toLocaleDateString([], { month: 'short', day: 'numeric' })})`
|
||||||
|
} else {
|
||||||
|
return `Last 30 Days (${startDate.toLocaleDateString([], { month: 'short', day: 'numeric' })} - ${now.toLocaleDateString([], { month: 'short', day: 'numeric' })})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directly hardcode the y-axis maximum in each chart option
|
||||||
|
const commonOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest' as const,
|
||||||
|
axis: 'x' as const,
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
stepSize: 25,
|
||||||
|
autoSkip: false,
|
||||||
|
callback: function(value: any) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Usage %'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
tension: 0.4,
|
||||||
|
spanGaps: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create charts with very explicit y-axis max values
|
||||||
|
const cpuCanvas = document.getElementById(`cpu-chart`) as HTMLCanvasElement
|
||||||
|
if (cpuCanvas) {
|
||||||
|
cpuChartRef.current = new Chart(cpuCanvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'CPU Usage',
|
||||||
|
data: history.datasets.cpu,
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
spanGaps: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'CPU Usage History',
|
||||||
|
font: {
|
||||||
|
size: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: function(tooltipItems: any) {
|
||||||
|
return timeLabels[tooltipItems[0].dataIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
...commonOptions.scales,
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
max: 100 // Force this to ensure it's applied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ramCanvas = document.getElementById(`ram-chart`) as HTMLCanvasElement
|
||||||
|
if (ramCanvas) {
|
||||||
|
ramChartRef.current = new Chart(ramCanvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'RAM Usage',
|
||||||
|
data: history.datasets.ram,
|
||||||
|
borderColor: 'rgb(153, 102, 255)',
|
||||||
|
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
spanGaps: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'RAM Usage History',
|
||||||
|
font: {
|
||||||
|
size: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: function(tooltipItems: any) {
|
||||||
|
return timeLabels[tooltipItems[0].dataIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
...commonOptions.scales,
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
max: 100 // Force this to ensure it's applied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskCanvas = document.getElementById(`disk-chart`) as HTMLCanvasElement
|
||||||
|
if (diskCanvas) {
|
||||||
|
diskChartRef.current = new Chart(diskCanvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Disk Usage',
|
||||||
|
data: history.datasets.disk,
|
||||||
|
borderColor: 'rgb(255, 159, 64)',
|
||||||
|
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
spanGaps: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Disk Usage History',
|
||||||
|
font: {
|
||||||
|
size: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: function(tooltipItems: any) {
|
||||||
|
return timeLabels[tooltipItems[0].dataIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
...commonOptions.scales,
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
max: 100 // Force this to ensure it's applied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(initTimer);
|
||||||
|
if (cpuChartRef.current) cpuChartRef.current.destroy();
|
||||||
|
if (ramChartRef.current) ramChartRef.current.destroy();
|
||||||
|
if (diskChartRef.current) diskChartRef.current.destroy();
|
||||||
|
};
|
||||||
|
}, [server, timeRange]);
|
||||||
|
|
||||||
|
// Function to refresh data
|
||||||
|
const refreshData = () => {
|
||||||
|
fetchServerDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
|
<AppSidebar />
|
||||||
|
<SidebarInset>
|
||||||
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="flex items-center gap-2 px-4">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem className="hidden md:block">
|
||||||
|
<BreadcrumbPage>/</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<NextLink href="/dashboard/servers" className="hover:underline">
|
||||||
|
<BreadcrumbPage>Servers</BreadcrumbPage>
|
||||||
|
</NextLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
{server && (
|
||||||
|
<>
|
||||||
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>{server.name}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="inline-block" role="status" aria-label="loading">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 stroke-white animate-spin "
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_9023_61563)">
|
||||||
|
<path
|
||||||
|
d="M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444"
|
||||||
|
stroke="stroke-current"
|
||||||
|
strokeWidth="1.4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
className="my-path"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_9023_61563">
|
||||||
|
<rect width="24" height="24" fill="white"></rect>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : server ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Server header card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{server.icon && <DynamicIcon name={server.icon as any} size={32} />}
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-2xl flex items-center gap-2">
|
||||||
|
{server.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{server.os || "No OS specified"} • {server.isVM ? "Virtual Machine" : "Physical Server"}
|
||||||
|
{server.isVM && server.hostServer && (
|
||||||
|
<> • Hosted on {server.hostedVMs?.[0]?.name}</>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{server.monitoring && (
|
||||||
|
<div className="absolute top-0 right-4">
|
||||||
|
<StatusIndicator isOnline={server.online} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium">Hardware</h3>
|
||||||
|
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||||
|
<div className="text-muted-foreground">CPU:</div>
|
||||||
|
<div>{server.cpu || "-"}</div>
|
||||||
|
<div className="text-muted-foreground">GPU:</div>
|
||||||
|
<div>{server.gpu || "-"}</div>
|
||||||
|
<div className="text-muted-foreground">RAM:</div>
|
||||||
|
<div>{server.ram || "-"}</div>
|
||||||
|
<div className="text-muted-foreground">Disk:</div>
|
||||||
|
<div>{server.disk || "-"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium">Network</h3>
|
||||||
|
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||||
|
<div className="text-muted-foreground">IP Address:</div>
|
||||||
|
<div>{server.ip || "-"}</div>
|
||||||
|
<div className="text-muted-foreground">Management URL:</div>
|
||||||
|
<div>
|
||||||
|
{server.url ? (
|
||||||
|
<a href={server.url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 text-blue-500 hover:underline">
|
||||||
|
{server.url} <Link className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{server.monitoring && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium">Current Usage</h3>
|
||||||
|
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||||
|
<div className="text-muted-foreground">CPU Usage:</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${server.cpuUsage > 80 ? "bg-destructive" : server.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${server.cpuUsage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{server.cpuUsage}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">RAM Usage:</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${server.ramUsage > 80 ? "bg-destructive" : server.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${server.ramUsage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{server.ramUsage}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">Disk Usage:</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${server.diskUsage > 80 ? "bg-destructive" : server.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${server.diskUsage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{server.diskUsage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Charts */}
|
||||||
|
{server.monitoring && server.history && (
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Resource Usage History</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{timeRange === '1h'
|
||||||
|
? 'Last hour, per minute'
|
||||||
|
: timeRange === '1d'
|
||||||
|
? 'Last 24 hours, 15-minute intervals'
|
||||||
|
: timeRange === '7d'
|
||||||
|
? 'Last 7 days, hourly intervals'
|
||||||
|
: 'Last 30 days, 4-hour intervals'}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={timeRange} onValueChange={(value: '1h' | '1d' | '7d' | '30d') => setTimeRange(value)}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Time range" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1h">Last Hour</SelectItem>
|
||||||
|
<SelectItem value="1d">Last 24 Hours</SelectItem>
|
||||||
|
<SelectItem value="7d">Last 7 Days</SelectItem>
|
||||||
|
<SelectItem value="30d">Last 30 Days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="outline" onClick={refreshData}>Refresh</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 gap-8">
|
||||||
|
<div className="h-[200px] relative bg-background">
|
||||||
|
<canvas id="cpu-chart" />
|
||||||
|
</div>
|
||||||
|
<div className="h-[200px] relative bg-background">
|
||||||
|
<canvas id="ram-chart" />
|
||||||
|
</div>
|
||||||
|
<div className="h-[200px] relative bg-background">
|
||||||
|
<canvas id="disk-chart" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Virtual Machines */}
|
||||||
|
{server.hostedVMs && server.hostedVMs.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Virtual Machines</CardTitle>
|
||||||
|
<CardDescription>Virtual machines hosted on this server</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{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="flex items-center gap-2">
|
||||||
|
{hostedVM.icon && (
|
||||||
|
<DynamicIcon
|
||||||
|
name={hostedVM.icon as any}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NextLink href={`/dashboard/servers/${hostedVM.id}`} className="hover:underline">
|
||||||
|
<div className="text-base font-extrabold">
|
||||||
|
{hostedVM.icon && "・ "}
|
||||||
|
{hostedVM.name}
|
||||||
|
</div>
|
||||||
|
</NextLink>
|
||||||
|
</div>
|
||||||
|
{hostedVM.monitoring && (
|
||||||
|
<StatusIndicator isOnline={hostedVM.online} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full pb-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="col-span-full mb-2">
|
||||||
|
<h4 className="text-sm font-semibold">Hardware Information</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-foreground/80">
|
||||||
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{hostedVM.monitoring && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-full pt-2 pb-2">
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">CPU</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{hostedVM.cpuUsage || 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
|
||||||
|
<div
|
||||||
|
className={`h-full ${hostedVM.cpuUsage && hostedVM.cpuUsage > 80 ? "bg-destructive" : hostedVM.cpuUsage && hostedVM.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${hostedVM.cpuUsage || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">RAM</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{hostedVM.ramUsage || 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
|
||||||
|
<div
|
||||||
|
className={`h-full ${hostedVM.ramUsage && hostedVM.ramUsage > 80 ? "bg-destructive" : hostedVM.ramUsage && hostedVM.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${hostedVM.ramUsage || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Disk</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{hostedVM.diskUsage || 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary mt-1">
|
||||||
|
<div
|
||||||
|
className={`h-full ${hostedVM.diskUsage && hostedVM.diskUsage > 80 ? "bg-destructive" : hostedVM.diskUsage && hostedVM.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||||
|
style={{ width: `${hostedVM.diskUsage || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center p-12">
|
||||||
|
<h2 className="text-2xl font-bold">Server not found</h2>
|
||||||
|
<p className="text-muted-foreground mt-2">The requested server could not be found or you don't have permission to view it.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
59
app/dashboard/servers/[server_id]/page.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import ServerDetail from "./Server"
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isAuthChecked, setIsAuthChecked] = useState(false);
|
||||||
|
const [isValid, setIsValid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = Cookies.get("token");
|
||||||
|
if (!token) {
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
const checkToken = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/auth/validate", {
|
||||||
|
token: token,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
setIsValid(true);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Cookies.remove("token");
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkToken();
|
||||||
|
}
|
||||||
|
setIsAuthChecked(true);
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (!isAuthChecked) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className='inline-block' role='status' aria-label='loading'>
|
||||||
|
<svg className='w-6 h-6 stroke-white animate-spin ' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<g clipPath='url(#clip0_9023_61563)'>
|
||||||
|
<path d='M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444' stroke='stroke-current' strokeWidth='1.4' strokeLinecap='round' className='my-path'></path>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='clip0_9023_61563'>
|
||||||
|
<rect width='24' height='24' fill='white'></rect>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<span className='sr-only'>Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid ? <ServerDetail /> : null;
|
||||||
|
}
|
||||||
@@ -1,42 +1,52 @@
|
|||||||
import { AppSidebar } from "@/components/app-sidebar";
|
"use client"
|
||||||
|
|
||||||
|
import { AppSidebar } from "@/components/app-sidebar"
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb"
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator"
|
||||||
import {
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||||
SidebarInset,
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
SidebarProvider,
|
import { useTheme } from "next-themes"
|
||||||
SidebarTrigger,
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
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, AtSign, Send, MessageSquare, Trash2, Play } from "lucide-react"
|
||||||
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
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_application?: string
|
||||||
|
notification_text_server?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
const [email, setEmail] = useState<string>("")
|
const [email, setEmail] = useState<string>("")
|
||||||
const [password, setPassword] = useState<string>("")
|
const [password, setPassword] = useState<string>("")
|
||||||
@@ -51,86 +61,212 @@ 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 [gotifyUrl, setGotifyUrl] = useState<string>("")
|
||||||
|
const [gotifyToken, setGotifyToken] = useState<string>("")
|
||||||
|
const [ntfyUrl, setNtfyUrl] = useState<string>("")
|
||||||
|
const [ntfyToken, setNtfyToken] = useState<string>("")
|
||||||
|
const [pushoverUrl, setPushoverUrl] = useState<string>("")
|
||||||
|
const [pushoverToken, setPushoverToken] = useState<string>("")
|
||||||
|
const [pushoverUser, setPushoverUser] = useState<string>("")
|
||||||
|
|
||||||
|
const [notifications, setNotifications] = useState<any[]>([])
|
||||||
|
|
||||||
|
const [notificationTextApplication, setNotificationTextApplication] = useState<string>("")
|
||||||
|
const [notificationTextServer, setNotificationTextServer] = useState<string>("")
|
||||||
|
|
||||||
const changeEmail = async () => {
|
const changeEmail = async () => {
|
||||||
setEmailErrorVisible(false);
|
setEmailErrorVisible(false)
|
||||||
setEmailSuccess(false);
|
setEmailSuccess(false)
|
||||||
setEmailError("");
|
setEmailError("")
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
setEmailError("Email is required");
|
setEmailError("Email is required")
|
||||||
setEmailErrorVisible(true);
|
setEmailErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setEmailErrorVisible(false);
|
setEmailErrorVisible(false)
|
||||||
setEmailError("");
|
setEmailError("")
|
||||||
}
|
}, 3000)
|
||||||
, 3000);
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/auth/edit_email', {
|
await axios.post("/api/auth/edit_email", {
|
||||||
newEmail: email,
|
newEmail: email,
|
||||||
jwtToken: Cookies.get('token')
|
jwtToken: Cookies.get("token"),
|
||||||
});
|
})
|
||||||
setEmailSuccess(true);
|
setEmailSuccess(true)
|
||||||
setEmail("");
|
setEmail("")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setEmailSuccess(false);
|
setEmailSuccess(false)
|
||||||
}, 3000);
|
}, 3000)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setEmailError(error.response.data.error);
|
setEmailError(error.response.data.error)
|
||||||
setEmailErrorVisible(true);
|
setEmailErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setEmailErrorVisible(false);
|
setEmailErrorVisible(false)
|
||||||
setEmailError("");
|
setEmailError("")
|
||||||
}, 3000);
|
}, 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
try {
|
try {
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
setPasswordError("Passwords do not match");
|
setPasswordError("Passwords do not match")
|
||||||
setPasswordErrorVisible(true);
|
setPasswordErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordErrorVisible(false);
|
setPasswordErrorVisible(false)
|
||||||
setPasswordError("");
|
setPasswordError("")
|
||||||
}, 3000);
|
}, 3000)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (!oldPassword || !password || !confirmPassword) {
|
if (!oldPassword || !password || !confirmPassword) {
|
||||||
setPasswordError("All fields are required");
|
setPasswordError("All fields are required")
|
||||||
setPasswordErrorVisible(true);
|
setPasswordErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordErrorVisible(false);
|
setPasswordErrorVisible(false)
|
||||||
setPasswordError("");
|
setPasswordError("")
|
||||||
}, 3000);
|
}, 3000)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.post('/api/auth/edit_password', {
|
const response = await axios.post("/api/auth/edit_password", {
|
||||||
oldPassword: oldPassword,
|
oldPassword: oldPassword,
|
||||||
newPassword: password,
|
newPassword: password,
|
||||||
jwtToken: Cookies.get('token')
|
jwtToken: Cookies.get("token"),
|
||||||
});
|
})
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setPasswordSuccess(true);
|
setPasswordSuccess(true)
|
||||||
setPassword("");
|
setPassword("")
|
||||||
setOldPassword("");
|
setOldPassword("")
|
||||||
setConfirmPassword("");
|
setConfirmPassword("")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordSuccess(false);
|
setPasswordSuccess(false)
|
||||||
}, 3000);
|
}, 3000)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setPasswordErrorVisible(true);
|
setPasswordErrorVisible(true)
|
||||||
setPasswordError(error.response.data.error);
|
setPasswordError(error.response.data.error)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordErrorVisible(false);
|
setPasswordErrorVisible(false)
|
||||||
setPasswordError("");
|
setPasswordError("")
|
||||||
}, 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,
|
||||||
|
gotifyUrl: gotifyUrl,
|
||||||
|
gotifyToken: gotifyToken,
|
||||||
|
ntfyUrl: ntfyUrl,
|
||||||
|
ntfyToken: ntfyToken,
|
||||||
|
pushoverUrl: pushoverUrl,
|
||||||
|
pushoverToken: pushoverToken,
|
||||||
|
pushoverUser: pushoverUser,
|
||||||
|
})
|
||||||
|
getNotifications()
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNotification = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/notifications/delete", {
|
||||||
|
id: id,
|
||||||
|
})
|
||||||
|
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_application) {
|
||||||
|
setNotificationTextApplication(response.data.notification_text_application)
|
||||||
|
} else {
|
||||||
|
setNotificationTextApplication("The application !name (!url) is now !status.")
|
||||||
|
}
|
||||||
|
if (response.data.notification_text_server) {
|
||||||
|
setNotificationTextServer(response.data.notification_text_server)
|
||||||
|
} else {
|
||||||
|
setNotificationTextServer("The server !name is now !status.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editNotificationText = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/settings/notification_text", {
|
||||||
|
text_application: notificationTextApplication,
|
||||||
|
text_server: notificationTextServer,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response.data.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNotificationText()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const testNotification = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/notifications/test", {
|
||||||
|
notificationId: id,
|
||||||
|
})
|
||||||
|
toast.success("Notification will be sent in a few seconds.")
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response.data.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
@@ -289,6 +425,396 @@ 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-3">
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold">Notifications</h2>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="text-sm text-muted-foreground mb-6">
|
||||||
|
Set up notifications to get instantly alerted when an application changes status.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button className="w-full h-11 flex items-center gap-2">
|
||||||
|
Add Notification Channel
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogTitle>Add Notification</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Notification Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="smtp">SMTP</SelectItem>
|
||||||
|
<SelectItem value="telegram">Telegram</SelectItem>
|
||||||
|
<SelectItem value="discord">Discord</SelectItem>
|
||||||
|
<SelectItem value="gotify">Gotify</SelectItem>
|
||||||
|
<SelectItem value="ntfy">Ntfy</SelectItem>
|
||||||
|
<SelectItem value="pushover">Pushover</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
|
||||||
|
{notificationType === "smtp" && (
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="smtpHost">SMTP Host</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="smtpHost"
|
||||||
|
placeholder="smtp.example.com"
|
||||||
|
onChange={(e) => setSmtpHost(e.target.value)}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "gotify" && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="gotifyUrl">Gotify URL</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="gotifyUrl"
|
||||||
|
placeholder=""
|
||||||
|
onChange={(e) => setGotifyUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="gotifyToken">Gotify Token</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="gotifyToken"
|
||||||
|
placeholder=""
|
||||||
|
onChange={(e) => setGotifyToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "ntfy" && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="ntfyUrl"
|
||||||
|
placeholder=""
|
||||||
|
onChange={(e) => setNtfyUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="ntfyToken">Ntfy Token</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="ntfyToken"
|
||||||
|
placeholder=""
|
||||||
|
onChange={(e) => setNtfyToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "pushover" && (
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="pushoverUrl">Pushover URL</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="pushoverUrl"
|
||||||
|
placeholder="e.g. https://api.pushover.net/1/messages.json"
|
||||||
|
onChange={(e) => setPushoverUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="pushoverToken">Pushover Token</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="pushoverToken"
|
||||||
|
placeholder="e.g. 1234567890"
|
||||||
|
onChange={(e) => setPushoverToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid w-full items-center gap-1.5">
|
||||||
|
<Label htmlFor="pushoverUser">Pushover User</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="pushoverUser"
|
||||||
|
placeholder="e.g. 1234567890"
|
||||||
|
onChange={(e) => setPushoverUser(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button className="w-full h-11" variant="outline">
|
||||||
|
Customize Notification Text
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="text_application">Notification Text for Applications</Label>
|
||||||
|
<Textarea
|
||||||
|
id="text_application"
|
||||||
|
placeholder="Type here..."
|
||||||
|
value={notificationTextApplication}
|
||||||
|
onChange={(e) => setNotificationTextApplication(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="text_server">Notification Text for Servers</Label>
|
||||||
|
<Textarea
|
||||||
|
id="text_server"
|
||||||
|
placeholder="Type here..."
|
||||||
|
value={notificationTextServer}
|
||||||
|
onChange={(e) => setNotificationTextServer(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 text-sm text-muted-foreground">
|
||||||
|
You can use the following placeholders in the text:
|
||||||
|
<ul className="list-disc list-inside space-y-1 pt-2">
|
||||||
|
<li>
|
||||||
|
<b>Server related:</b>
|
||||||
|
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
||||||
|
<li>!name - The name of the server</li>
|
||||||
|
<li>!status - The current status of the server (online/offline)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Application related:</b>
|
||||||
|
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
||||||
|
<li>!name - The name of the application</li>
|
||||||
|
<li>!url - The URL where the application is hosted</li>
|
||||||
|
<li>!status - The current status of the application (online/offline)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Active Notification Channels</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{notifications.length > 0 ? (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className="flex items-center justify-between p-4 rounded-lg border bg-card transition-all hover:shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{notification.type === "smtp" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<AtSign className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.type === "telegram" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<Send className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.type === "discord" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<MessageSquare className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.type === "gotify" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.type === "ntfy" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.type === "pushover" && (
|
||||||
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium capitalize">{notification.type}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{notification.type === "smtp" && "Email notifications"}
|
||||||
|
{notification.type === "telegram" && "Telegram bot alerts"}
|
||||||
|
{notification.type === "discord" && "Discord webhook alerts"}
|
||||||
|
{notification.type === "gotify" && "Gotify notifications"}
|
||||||
|
{notification.type === "ntfy" && "Ntfy notifications"}
|
||||||
|
{notification.type === "pushover" && "Pushover notifications"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="hover:bg-muted/20"
|
||||||
|
onClick={() => testNotification(notification.id)}
|
||||||
|
>
|
||||||
|
<Play className="h-4 w-4 mr-1" />
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="hover:bg-muted/20"
|
||||||
|
onClick={() => deleteNotification(notification.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 border rounded-lg bg-muted/5">
|
||||||
|
<div className="flex justify-center mb-3">
|
||||||
|
<div className="bg-muted/20 p-3 rounded-full">
|
||||||
|
<Bell className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-1">No notifications configured</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||||
|
Add a notification channel to get alerted when your applications change status.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|||||||
@@ -34,14 +34,18 @@ const timeFormats = {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}),
|
}),
|
||||||
2: (timestamp: string) => {
|
2: (timestamp: string) =>
|
||||||
const start = new Date(timestamp);
|
new Date(timestamp).toLocaleTimeString([], {
|
||||||
const end = new Date(start.getTime() + 3 * 60 * 60 * 1000);
|
hour: '2-digit',
|
||||||
return `${start.toLocaleDateString([], { day: '2-digit', month: 'short' })}
|
minute: '2-digit',
|
||||||
${start.getHours().toString().padStart(2, '0')}:00 -
|
hour12: false
|
||||||
${end.getHours().toString().padStart(2, '0')}:00`;
|
}),
|
||||||
},
|
|
||||||
3: (timestamp: string) =>
|
3: (timestamp: string) =>
|
||||||
|
new Date(timestamp).toLocaleDateString([], {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short'
|
||||||
|
}),
|
||||||
|
4: (timestamp: string) =>
|
||||||
new Date(timestamp).toLocaleDateString([], {
|
new Date(timestamp).toLocaleDateString([], {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'short'
|
month: 'short'
|
||||||
@@ -49,9 +53,10 @@ const timeFormats = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const minBoxWidths = {
|
const minBoxWidths = {
|
||||||
1: 24,
|
1: 20,
|
||||||
2: 24,
|
2: 20,
|
||||||
3: 24
|
3: 24,
|
||||||
|
4: 24
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UptimeData {
|
interface UptimeData {
|
||||||
@@ -72,7 +77,7 @@ interface PaginationData {
|
|||||||
|
|
||||||
export default function Uptime() {
|
export default function Uptime() {
|
||||||
const [data, setData] = useState<UptimeData[]>([]);
|
const [data, setData] = useState<UptimeData[]>([]);
|
||||||
const [timespan, setTimespan] = useState<1 | 2 | 3>(1);
|
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
|
||||||
const [pagination, setPagination] = useState<PaginationData>({
|
const [pagination, setPagination] = useState<PaginationData>({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
@@ -153,7 +158,7 @@ export default function Uptime() {
|
|||||||
<Select
|
<Select
|
||||||
value={String(timespan)}
|
value={String(timespan)}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setTimespan(Number(v) as 1 | 2 | 3);
|
setTimespan(Number(v) as 1 | 2 | 3 | 4);
|
||||||
setPagination(prev => ({...prev, currentPage: 1}));
|
setPagination(prev => ({...prev, currentPage: 1}));
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -162,9 +167,10 @@ export default function Uptime() {
|
|||||||
<SelectValue placeholder="Select timespan" />
|
<SelectValue placeholder="Select timespan" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">Last 30 minutes</SelectItem>
|
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||||
<SelectItem value="2">Last 7 days</SelectItem>
|
<SelectItem value="2">Last 1 day</SelectItem>
|
||||||
<SelectItem value="3">Last 30 days</SelectItem>
|
<SelectItem value="3">Last 7 days</SelectItem>
|
||||||
|
<SelectItem value="4">Last 30 days</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,18 +225,14 @@ export default function Uptime() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{timespan === 2 ? (
|
{new Date(entry.timestamp).toLocaleString([], {
|
||||||
timeFormats[2](entry.timestamp)
|
year: 'numeric',
|
||||||
) : (
|
month: 'short',
|
||||||
new Date(entry.timestamp).toLocaleString([], {
|
day: timespan > 2 ? 'numeric' : undefined,
|
||||||
year: 'numeric',
|
hour: '2-digit',
|
||||||
month: 'short',
|
minute: timespan === 1 ? '2-digit' : undefined,
|
||||||
day: 'numeric',
|
hour12: false
|
||||||
hour: '2-digit',
|
})}
|
||||||
minute: timespan === 3 ? undefined : '2-digit',
|
|
||||||
hour12: false
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{entry.missing
|
{entry.missing
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -59,7 +77,7 @@ const data: { navMain: NavItem[] } = {
|
|||||||
title: "Network",
|
title: "Network",
|
||||||
icon: Network,
|
icon: Network,
|
||||||
url: "/dashboard/network",
|
url: "/dashboard/network",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -72,71 +90,119 @@ 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>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</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">
|
||||||
<SidebarMenu>
|
<SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2">
|
||||||
{data.navMain.map((item) => (
|
Main Navigation
|
||||||
<SidebarMenuItem key={item.title}>
|
</SidebarGroupLabel>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarGroupContent>
|
||||||
<Link href={item.url} className="font-medium">
|
<SidebarMenu>
|
||||||
{item.icon && <item.icon className="mr-2" />}
|
{data.navMain.map((item) =>
|
||||||
{item.title}
|
item.items?.length ? (
|
||||||
</Link>
|
<Collapsible key={item.title} defaultOpen={hasActiveChild(item.items)} className="group/collapsible">
|
||||||
</SidebarMenuButton>
|
<SidebarMenuItem>
|
||||||
{item.items?.length && (
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuButton
|
||||||
{item.items.map((subItem) => (
|
className={cn(
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
"font-medium transition-all",
|
||||||
<SidebarMenuSubButton
|
(hasActiveChild(item.items) || isActive(item.url)) &&
|
||||||
asChild
|
"text-sidebar-accent-foreground bg-sidebar-accent/50",
|
||||||
isActive={subItem.isActive ?? false}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={subItem.url}>
|
{item.icon && <item.icon className="h-4 w-4" />}
|
||||||
{subItem.icon && <subItem.icon className="mr-2" />}
|
<span>{item.title}</span>
|
||||||
{subItem.title}
|
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||||
</Link>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuSubButton>
|
</CollapsibleTrigger>
|
||||||
</SidebarMenuSubItem>
|
<CollapsibleContent>
|
||||||
))}
|
<SidebarMenuSub>
|
||||||
</SidebarMenuSub>
|
{item.items.map((subItem) => (
|
||||||
)}
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
</SidebarMenuItem>
|
<SidebarMenuSubButton asChild isActive={isActive(subItem.url)} className="transition-all">
|
||||||
))}
|
<Link href={subItem.url} className="flex items-center">
|
||||||
</SidebarMenu>
|
{subItem.icon && <subItem.icon className="h-3.5 w-3.5 mr-2" />}
|
||||||
|
<span>{subItem.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</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>
|
||||||
|
</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 />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
38
components/status-indicator.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
|
||||||
|
interface StatusIndicatorProps {
|
||||||
|
isOnline?: boolean
|
||||||
|
className?: string
|
||||||
|
showLabel?: boolean
|
||||||
|
pulseAnimation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusIndicator({
|
||||||
|
isOnline = false,
|
||||||
|
className,
|
||||||
|
showLabel = true,
|
||||||
|
pulseAnimation = true,
|
||||||
|
}: StatusIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-2 py-1 border-transparent transition-colors duration-300",
|
||||||
|
isOnline
|
||||||
|
? "bg-green-100 dark:bg-green-950/30 text-green-800 dark:text-green-300"
|
||||||
|
: "bg-red-100 dark:bg-red-950/30 text-red-800 dark:text-red-300",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn("relative flex h-2.5 w-2.5 rounded-full", isOnline ? "bg-green-500" : "bg-red-500")}>
|
||||||
|
{isOnline && pulseAnimation && (
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{showLabel && <span className="text-xs font-medium">{isOnline ? "Online" : "Offline"}</span>}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
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
@@ -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
@@ -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 }
|
||||||
31
components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
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 }
|
||||||
25
components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
11
compose.yml
@@ -6,14 +6,14 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
- agent
|
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
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 +24,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:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 80 KiB |
66
docs/.vitepress/config.mts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
title: "CoreControl",
|
||||||
|
description: "Dashboard to manage your entire server infrastructure",
|
||||||
|
lastUpdated: true,
|
||||||
|
cleanUrls: true,
|
||||||
|
metaChunk: true,
|
||||||
|
head: [
|
||||||
|
['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }],
|
||||||
|
],
|
||||||
|
themeConfig: {
|
||||||
|
logo: '/logo.png',
|
||||||
|
nav: [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Installation', link: '/installation' }
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the MIT License.',
|
||||||
|
copyright: 'Copyright © 2025-present CoreControl',
|
||||||
|
},
|
||||||
|
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
text: 'Deploy',
|
||||||
|
items: [
|
||||||
|
{ text: 'Installation', link: '/installation' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'General',
|
||||||
|
items: [
|
||||||
|
{ text: 'Dashboard', link: '/general/Dashboard' },
|
||||||
|
{ text: 'Servers', link: '/general/Servers' },
|
||||||
|
{ text: 'Applications', link: '/general/Applications' },
|
||||||
|
{ text: 'Uptime', link: '/general/Uptime' },
|
||||||
|
{ text: 'Network', link: '/general/Network' },
|
||||||
|
{ text: 'Settings', link: '/general/Settings' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Notifications',
|
||||||
|
items: [
|
||||||
|
{ text: 'General', link: '/notifications/General' },
|
||||||
|
{ text: 'Email', link: '/notifications/Email' },
|
||||||
|
{ text: 'Telegram', link: '/notifications/Telegram' },
|
||||||
|
{ text: 'Discord', link: '/notifications/Discord' },
|
||||||
|
{ text: 'Gotify', link: '/notifications/Gotify' },
|
||||||
|
{ text: 'Ntfy', link: '/notifications/Ntfy' },
|
||||||
|
{ text: 'Pushover', link: '/notifications/Pushover' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/crocofied/corecontrol' },
|
||||||
|
{ icon: 'buymeacoffee', link: 'https://www.buymeacoffee.com/corecontrol' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
23
docs/.vitepress/dist/404.html
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>404 | CoreControl</title>
|
||||||
|
<meta name="description" content="Not Found">
|
||||||
|
<meta name="generator" content="VitePress v1.6.3">
|
||||||
|
<link rel="preload stylesheet" href="/assets/style.DEOyzpKL.css" as="style">
|
||||||
|
<link rel="preload stylesheet" href="/vp-icons.css" as="style">
|
||||||
|
<script type="module" src="/assets/chunks/metadata.b7f24d28.js"></script>
|
||||||
|
<script type="module" src="/assets/app.DQZaLSC2.js"></script>
|
||||||
|
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||||
|
<link rel="icon" type="image/png" href="/logo.png">
|
||||||
|
<script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
|
||||||
|
<script id="check-mac-os">document.documentElement.classList.toggle("mac",/Mac|iPhone|iPod|iPad/i.test(navigator.platform));</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
docs/.vitepress/dist/assets/app.DQZaLSC2.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{t as p}from"./chunks/theme.BTnOYcHU.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.DPDPlp3K.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp};
|
||||||
BIN
docs/.vitepress/dist/assets/applications_add_button.CTBM75AA.png
vendored
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/.vitepress/dist/assets/applications_display.D1ZCmp63.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.CP7uK6Pq.js
vendored
Normal file
8
docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.B974QcpM.js
vendored
Normal file
18
docs/.vitepress/dist/assets/chunks/framework.DPDPlp3K.js
vendored
Normal file
1
docs/.vitepress/dist/assets/chunks/metadata.b7f24d28.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
window.__VP_HASH_MAP__=JSON.parse("{\"general_applications.md\":\"DFVqSlCw\",\"general_dashboard.md\":\"DW5yESFW\",\"general_network.md\":\"tbP8aEzX\",\"general_servers.md\":\"BaASA60T\",\"general_settings.md\":\"DrC2XV32\",\"general_uptime.md\":\"CKBdQg4u\",\"index.md\":\"_yXl4OkC\",\"installation.md\":\"Cz1eOHOr\",\"notifications_discord.md\":\"C0x5CxmR\",\"notifications_email.md\":\"Cugw2BRs\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"vFHjr6ko\",\"notifications_ntfy.md\":\"CPMnGQVP\",\"notifications_pushover.md\":\"lZwGAQ0A\",\"notifications_telegram.md\":\"B6_EzaEX\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"CoreControl\",\"description\":\"Dashboard to manage your entire server infrastructure\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"logo\":\"/logo.png\",\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Installation\",\"link\":\"/installation\"}],\"footer\":{\"message\":\"Released under the MIT License.\",\"copyright\":\"Copyright © 2025-present CoreControl\"},\"search\":{\"provider\":\"local\"},\"sidebar\":[{\"text\":\"Deploy\",\"items\":[{\"text\":\"Installation\",\"link\":\"/installation\"}]},{\"text\":\"General\",\"items\":[{\"text\":\"Dashboard\",\"link\":\"/general/Dashboard\"},{\"text\":\"Servers\",\"link\":\"/general/Servers\"},{\"text\":\"Applications\",\"link\":\"/general/Applications\"},{\"text\":\"Uptime\",\"link\":\"/general/Uptime\"},{\"text\":\"Network\",\"link\":\"/general/Network\"},{\"text\":\"Settings\",\"link\":\"/general/Settings\"}]},{\"text\":\"Notifications\",\"items\":[{\"text\":\"General\",\"link\":\"/notifications/General\"},{\"text\":\"Email\",\"link\":\"/notifications/Email\"},{\"text\":\"Telegram\",\"link\":\"/notifications/Telegram\"},{\"text\":\"Discord\",\"link\":\"/notifications/Discord\"},{\"text\":\"Gotify\",\"link\":\"/notifications/Gotify\"},{\"text\":\"Ntfy\",\"link\":\"/notifications/Ntfy\"},{\"text\":\"Pushover\",\"link\":\"/notifications/Pushover\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");
|
||||||
1
docs/.vitepress/dist/assets/chunks/settings_notifications.DL7eQG4d.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const s="/assets/settings_notifications.DwqFpmxq.png";export{s as _};
|
||||||
2
docs/.vitepress/dist/assets/chunks/theme.BTnOYcHU.js
vendored
Normal file
BIN
docs/.vitepress/dist/assets/dashboard_card_applications.DErIEBeJ.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_network.C2YTU2ur.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_servers.DNNhRbkY.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_uptime.B6cdZ6Ju.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
1
docs/.vitepress/dist/assets/general_Applications.md.DFVqSlCw.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i,c as a,o,ag as e}from"./chunks/framework.DPDPlp3K.js";const l="/assets/applications_add_button.CTBM75AA.png",n="/assets/applications_display.D1ZCmp63.png",_=JSON.parse('{"title":"Applications","description":"","frontmatter":{},"headers":[],"relativePath":"general/Applications.md","filePath":"general/Applications.md","lastUpdated":1745241280000}'),p={name:"general/Applications.md"};function r(s,t,c,d,h,u){return o(),a("div",null,t[0]||(t[0]=[e('<h1 id="applications" tabindex="-1">Applications <a class="header-anchor" href="#applications" aria-label="Permalink to "Applications""></a></h1><p>All your self-hosted applications are displayed here.</p><h2 id="add-an-application" tabindex="-1">Add an application <a class="header-anchor" href="#add-an-application" aria-label="Permalink to "Add an application""></a></h2><p>To add a new application to CoreControl, follow these steps:</p><ol><li><p>Click the "Add Application" button in the top right corner of the server menu: <img src="'+l+'" alt="Application Add Button"></p></li><li><p>Fill out the server details across the following information:</p></li></ol><ul><li><strong>Name</strong>: Enter the name of the application</li><li><strong>Server</strong>: Select the server on which the application is running</li><li><strong>Description</strong>: Enter a short (or long) description of the server</li><li><strong>Icon URL</strong>: Add the url pointing to the logo of the application. With the flash button the logo will be automatically selected.</li><li><strong>Public URL</strong>: Enter the public URL of your application. This will be used to track the uptime.</li><li><strong>Local URL</strong>: Enter the local URL of your application, i.e. the URL via which the application is only accessible in the local network</li></ul><p>After filling out the required information, click "Add" to add the application to CoreControl.</p><h2 id="application-display" tabindex="-1">Application Display <a class="header-anchor" href="#application-display" aria-label="Permalink to "Application Display""></a></h2><p>Your applications are displayed in a list or grid (depending on the display settings) - each application in its own card <img src="'+n+'" alt="Application card"></p>',9)]))}const f=i(p,[["render",r]]);export{_ as __pageData,f as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Applications.md.DFVqSlCw.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i,c as a,o,ag as e}from"./chunks/framework.DPDPlp3K.js";const l="/assets/applications_add_button.CTBM75AA.png",n="/assets/applications_display.D1ZCmp63.png",_=JSON.parse('{"title":"Applications","description":"","frontmatter":{},"headers":[],"relativePath":"general/Applications.md","filePath":"general/Applications.md","lastUpdated":1745241280000}'),p={name:"general/Applications.md"};function r(s,t,c,d,h,u){return o(),a("div",null,t[0]||(t[0]=[e("",9)]))}const f=i(p,[["render",r]]);export{_ as __pageData,f as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Dashboard.md.DW5yESFW.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r,c as e,o as s,ag as t}from"./chunks/framework.DPDPlp3K.js";const o="/assets/dashboard_card_servers.DNNhRbkY.png",i="/assets/dashboard_card_applications.DErIEBeJ.png",d="/assets/dashboard_card_uptime.B6cdZ6Ju.png",l="/assets/dashboard_card_network.C2YTU2ur.png",f=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"general/Dashboard.md","filePath":"general/Dashboard.md","lastUpdated":1745241280000}'),n={name:"general/Dashboard.md"};function c(p,a,h,u,m,_){return s(),e("div",null,a[0]||(a[0]=[t('<h1 id="dashboard" tabindex="-1">Dashboard <a class="header-anchor" href="#dashboard" aria-label="Permalink to "Dashboard""></a></h1><p>The dashboard is the most important place to get a quick overview of your infrastructure.</p><h2 id="cards-overview" tabindex="-1">Cards Overview <a class="header-anchor" href="#cards-overview" aria-label="Permalink to "Cards Overview""></a></h2><p>The dashboard is divided into 4 cards that provide different aspects of your infrastructure monitoring:</p><h3 id="servers-card" tabindex="-1">Servers Card <a class="header-anchor" href="#servers-card" aria-label="Permalink to "Servers Card""></a></h3><p><img src="'+o+'" alt="Servers Card"></p><p>The Servers card displays information about all your connected servers, including:</p><ul><li>Number of Physical Servers</li><li>Number of Virtual Servers</li></ul><h3 id="applications-card" tabindex="-1">Applications Card <a class="header-anchor" href="#applications-card" aria-label="Permalink to "Applications Card""></a></h3><p><img src="'+i+'" alt="Applications Card"></p><p>The Applications card shows you:</p><ul><li>Number of running applications across your infrastructure</li></ul><h3 id="uptime-card" tabindex="-1">Uptime Card <a class="header-anchor" href="#uptime-card" aria-label="Permalink to "Uptime Card""></a></h3><p><img src="'+d+'" alt="Uptime Card"></p><p>The Uptime card provides:</p><ul><li>Number of online applications</li></ul><h3 id="network-card" tabindex="-1">Network Card <a class="header-anchor" href="#network-card" aria-label="Permalink to "Network Card""></a></h3><p><img src="'+l+'" alt="Network Card"></p><p>The Network card displays:</p><ul><li>Sum of servers and applications</li></ul>',20)]))}const v=r(n,[["render",c]]);export{f as __pageData,v as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Dashboard.md.DW5yESFW.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r,c as e,o as s,ag as t}from"./chunks/framework.DPDPlp3K.js";const o="/assets/dashboard_card_servers.DNNhRbkY.png",i="/assets/dashboard_card_applications.DErIEBeJ.png",d="/assets/dashboard_card_uptime.B6cdZ6Ju.png",l="/assets/dashboard_card_network.C2YTU2ur.png",f=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"general/Dashboard.md","filePath":"general/Dashboard.md","lastUpdated":1745241280000}'),n={name:"general/Dashboard.md"};function c(p,a,h,u,m,_){return s(),e("div",null,a[0]||(a[0]=[t("",20)]))}const v=r(n,[["render",c]]);export{f as __pageData,v as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Network.md.tbP8aEzX.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r,c as a,o as n,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Network","description":"","frontmatter":{},"headers":[],"relativePath":"general/Network.md","filePath":"general/Network.md","lastUpdated":1745241280000}'),s={name:"general/Network.md"};function i(l,t,c,d,h,p){return n(),a("div",null,t[0]||(t[0]=[e("h1",{id:"network",tabindex:"-1"},[o("Network "),e("a",{class:"header-anchor",href:"#network","aria-label":'Permalink to "Network"'},"")],-1),e("p",null,"A network flowchart is automatically generated on this page, which shows the connections of your infrastructure. The main servers are displayed based on the main node “My Infrastrucutre”. Below this are the applications running directly on this server and next to it the VMs running on the server, if it is a host server. To the right of the VMs, all applications running on the respective VM are listed.",-1)]))}const w=r(s,[["render",i]]);export{u as __pageData,w as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Network.md.tbP8aEzX.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r,c as a,o as n,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Network","description":"","frontmatter":{},"headers":[],"relativePath":"general/Network.md","filePath":"general/Network.md","lastUpdated":1745241280000}'),s={name:"general/Network.md"};function i(l,t,c,d,h,p){return n(),a("div",null,t[0]||(t[0]=[e("h1",{id:"network",tabindex:"-1"},[o("Network "),e("a",{class:"header-anchor",href:"#network","aria-label":'Permalink to "Network"'},"")],-1),e("p",null,"A network flowchart is automatically generated on this page, which shows the connections of your infrastructure. The main servers are displayed based on the main node “My Infrastrucutre”. Below this are the applications running directly on this server and next to it the VMs running on the server, if it is a host server. To the right of the VMs, all applications running on the respective VM are listed.",-1)]))}const w=r(s,[["render",i]]);export{u as __pageData,w as default};
|
||||||
12
docs/.vitepress/dist/assets/general_Servers.md.BaASA60T.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import{_ as s,c as a,o as i,ag as t}from"./chunks/framework.DPDPlp3K.js";const r="/assets/servers_add_button.DqYHhWPq.png",n="/assets/servers_display.f8nBpTOs.png",l="/assets/servers_vms_button.CXHECWPE.png",o="/assets/servers_vms_list.C3B4ERR1.png",v=JSON.parse('{"title":"Servers","description":"","frontmatter":{},"headers":[],"relativePath":"general/Servers.md","filePath":"general/Servers.md","lastUpdated":1745241280000}'),h={name:"general/Servers.md"};function p(d,e,c,g,k,u){return i(),a("div",null,e[0]||(e[0]=[t('<h1 id="servers" tabindex="-1">Servers <a class="header-anchor" href="#servers" aria-label="Permalink to "Servers""></a></h1><p>In the server menu you can see all your servers and add more if required</p><h2 id="add-a-server" tabindex="-1">Add a Server <a class="header-anchor" href="#add-a-server" aria-label="Permalink to "Add a Server""></a></h2><p>To add a new server to CoreControl, follow these steps:</p><ol><li><p>Click the "Add Server" button in the top right corner of the server menu: <img src="'+r+`" alt="Servers Add Button"></p></li><li><p>Fill out the server details across the following tabs:</p></li></ol><h3 id="general-tab" tabindex="-1">General Tab <a class="header-anchor" href="#general-tab" aria-label="Permalink to "General Tab""></a></h3><p>Configure the basic server information:</p><ul><li><strong>Icon</strong>: Choose a custom icon for your server</li><li><strong>Name</strong>: Enter a descriptive name for the server</li><li><strong>Operating System</strong>: Select the server's operating system</li><li><strong>IP Address</strong>: Enter the server's IP address</li><li><strong>Management URL</strong>: Add the URL used to manage the server (optional)</li></ul><h3 id="hardware-tab" tabindex="-1">Hardware Tab <a class="header-anchor" href="#hardware-tab" aria-label="Permalink to "Hardware Tab""></a></h3><p>Specify the server's hardware specifications:</p><ul><li><strong>CPU</strong>: Enter CPU model and specifications</li><li><strong>GPU</strong>: Add graphics card details if applicable</li><li><strong>RAM</strong>: Specify the amount of RAM</li><li><strong>Disk</strong>: Enter storage capacity and configuration</li></ul><h3 id="virtualization-tab" tabindex="-1">Virtualization Tab <a class="header-anchor" href="#virtualization-tab" aria-label="Permalink to "Virtualization Tab""></a></h3><p>Configure virtualization settings:</p><ul><li><strong>Host Server Settings</strong>: <ul><li>Enable "Host Server" if this server will host virtual machines</li><li>Perfect for hypervisors like Proxmox, VMware, or similar</li></ul></li><li><strong>VM Settings</strong>: <ul><li>Select a host server if this server is a virtual machine</li><li>This creates a logical connection between the VM and its host</li></ul></li></ul><h3 id="monitoring-tab" tabindex="-1">Monitoring Tab <a class="header-anchor" href="#monitoring-tab" aria-label="Permalink to "Monitoring Tab""></a></h3><p>Set up server monitoring options (see "Monitoring" section for detailed information)</p><p>After filling out the required information, click "Add" to add the server to CoreControl.</p><h2 id="monitoring" tabindex="-1">Monitoring <a class="header-anchor" href="#monitoring" aria-label="Permalink to "Monitoring""></a></h2><p>If you want to monitor the hardware usage and status of your servers, you will have to enable monitoring in the monitoring tab.</p><p>After you have done this you need to install <a href="https://github.com/nicolargo/glances" target="_blank" rel="noreferrer">Glances</a> on the server. To help you with this, we have created a sample compose that you can simply copy. For detailed customizations, please refer to the <a href="https://glances.readthedocs.io/en/latest/" target="_blank" rel="noreferrer">Glances docs</a>.</p><div class="language-yaml vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">services</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> glances</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">nicolargo/glances:latest</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> container_name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">glances</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> restart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">unless-stopped</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> ports</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"61208:61208"</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> pid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"host"</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">/var/run/docker.sock:/var/run/docker.sock:ro</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">GLANCES_OPT=-w --disable-webui</span></span></code></pre></div><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>Please also make sure that CoreControl can reach the specified API URL of Glances. In addition, the Glances API URL should be specified in the format <code>http://<IP_OF_SERVER>:61208</code>.</p></div><h2 id="server-display" tabindex="-1">Server Display <a class="header-anchor" href="#server-display" aria-label="Permalink to "Server Display""></a></h2><p>Your servers are displayed in a list or grid (depending on the display settings) - each server in its own card <img src="`+n+'" alt="Server Card"></p><p>There are also three action buttons at the end of each card.</p><ul><li>Link Button - With this you can open the specified management URL of the server with one click</li><li>Delete Button - Direct deletion of the server</li><li>Edit Button - Customize the server with the same menu as when creating the server</li></ul><h2 id="vms" tabindex="-1">VMs <a class="header-anchor" href="#vms" aria-label="Permalink to "VMs""></a></h2><p>If a host server contains VMs, you can display them using the “VMs” button <img src="'+l+'" alt="VMs Button"></p><p>The associated VMs are then displayed in a clearly arranged list. <img src="'+o+'" alt="VM List"></p>',29)]))}const E=s(h,[["render",p]]);export{v as __pageData,E as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Servers.md.BaASA60T.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as s,c as a,o as i,ag as t}from"./chunks/framework.DPDPlp3K.js";const r="/assets/servers_add_button.DqYHhWPq.png",n="/assets/servers_display.f8nBpTOs.png",l="/assets/servers_vms_button.CXHECWPE.png",o="/assets/servers_vms_list.C3B4ERR1.png",v=JSON.parse('{"title":"Servers","description":"","frontmatter":{},"headers":[],"relativePath":"general/Servers.md","filePath":"general/Servers.md","lastUpdated":1745241280000}'),h={name:"general/Servers.md"};function p(d,e,c,g,k,u){return i(),a("div",null,e[0]||(e[0]=[t("",29)]))}const E=s(h,[["render",p]]);export{v as __pageData,E as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Settings.md.DrC2XV32.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as s,c as a,o as i,ag as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/settings_user.eib6RZVK.png",o="/assets/settings_theme.AZP0Uw0g.png",f=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745241280000}'),g={name:"general/Settings.md"};function l(c,t,_,h,m,p){return i(),a("div",null,t[0]||(t[0]=[n('<h1 id="settings" tabindex="-1">Settings <a class="header-anchor" href="#settings" aria-label="Permalink to "Settings""></a></h1><p>Here you can manage the complete settings of CoreControl.</p><h2 id="user-settings" tabindex="-1">User Settings <a class="header-anchor" href="#user-settings" aria-label="Permalink to "User Settings""></a></h2><p><img src="'+r+'" alt="User Settings"></p><h2 id="theme-settings" tabindex="-1">Theme Settings <a class="header-anchor" href="#theme-settings" aria-label="Permalink to "Theme Settings""></a></h2><p><img src="'+o+'" alt="Theme Settings"></p><h2 id="notification-settings" tabindex="-1">Notification Settings <a class="header-anchor" href="#notification-settings" aria-label="Permalink to "Notification Settings""></a></h2><p><img src="'+e+'" alt="Notification Settings"></p>',8)]))}const u=s(g,[["render",l]]);export{f as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Settings.md.DrC2XV32.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as s,c as a,o as i,ag as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/settings_user.eib6RZVK.png",o="/assets/settings_theme.AZP0Uw0g.png",f=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745241280000}'),g={name:"general/Settings.md"};function l(c,t,_,h,m,p){return i(),a("div",null,t[0]||(t[0]=[n("",8)]))}const u=s(g,[["render",l]]);export{f as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Uptime.md.CKBdQg4u.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as a,c as i,o as s,j as e,a as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/uptime.Dt6hpqNV.png",f=JSON.parse('{"title":"Uptime","description":"","frontmatter":{},"headers":[],"relativePath":"general/Uptime.md","filePath":"general/Uptime.md","lastUpdated":1745241280000}'),p={name:"general/Uptime.md"};function l(o,t,m,c,d,u){return s(),i("div",null,t[0]||(t[0]=[e("h1",{id:"uptime",tabindex:"-1"},[n("Uptime "),e("a",{class:"header-anchor",href:"#uptime","aria-label":'Permalink to "Uptime"'},"")],-1),e("p",null,"The uptime of all your Applications is shown here in a clear list.",-1),e("p",null,[e("img",{src:r,alt:"Uptime"})],-1),e("p",null,"With the Select menu you can also filter the time span (30min, 7 days and 30 days)",-1)]))}const _=a(p,[["render",l]]);export{f as __pageData,_ as default};
|
||||||
1
docs/.vitepress/dist/assets/general_Uptime.md.CKBdQg4u.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as a,c as i,o as s,j as e,a as n}from"./chunks/framework.DPDPlp3K.js";const r="/assets/uptime.Dt6hpqNV.png",f=JSON.parse('{"title":"Uptime","description":"","frontmatter":{},"headers":[],"relativePath":"general/Uptime.md","filePath":"general/Uptime.md","lastUpdated":1745241280000}'),p={name:"general/Uptime.md"};function l(o,t,m,c,d,u){return s(),i("div",null,t[0]||(t[0]=[e("h1",{id:"uptime",tabindex:"-1"},[n("Uptime "),e("a",{class:"header-anchor",href:"#uptime","aria-label":'Permalink to "Uptime"'},"")],-1),e("p",null,"The uptime of all your Applications is shown here in a clear list.",-1),e("p",null,[e("img",{src:r,alt:"Uptime"})],-1),e("p",null,"With the Select menu you can also filter the time span (30min, 7 days and 30 days)",-1)]))}const _=a(p,[["render",l]]);export{f as __pageData,_ as default};
|
||||||
1
docs/.vitepress/dist/assets/index.md._yXl4OkC.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed withalerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};
|
||||||
1
docs/.vitepress/dist/assets/index.md._yXl4OkC.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed withalerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};
|
||||||
36
docs/.vitepress/dist/assets/installation.md.Cz1eOHOr.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Installation","description":"","frontmatter":{},"headers":[],"relativePath":"installation.md","filePath":"installation.md","lastUpdated":1745171698000}'),l={name:"installation.md"};function e(p,s,h,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="installation" tabindex="-1">Installation <a class="header-anchor" href="#installation" aria-label="Permalink to "Installation""></a></h1><p>The easiest way to install CoreControl is using Docker Compose. Follow these steps:</p><h2 id="docker-compose-installation" tabindex="-1">Docker Compose Installation <a class="header-anchor" href="#docker-compose-installation" aria-label="Permalink to "Docker Compose Installation""></a></h2><div class="danger custom-block"><p class="custom-block-title">DANGER</p><p>CoreControl is at an early stage of development and is subject to change. It is not recommended for use in a production environment at this time.</p></div><ol><li><p>Make sure <a href="https://docs.docker.com/get-docker/" target="_blank" rel="noreferrer">Docker</a> and <a href="https://docs.docker.com/compose/install/" target="_blank" rel="noreferrer">Docker Compose</a> are installed on your system.</p></li><li><p>Create a file named <code>docker-compose.yml</code> with the following content:</p></li></ol><div class="language-yaml vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">services</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> web</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">haedlessdev/corecontrol:latest</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> ports</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"3000:3000"</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> JWT_SECRET</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">RANDOM_SECRET</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Replace with a secure random string</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> DATABASE_URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"postgresql://postgres:postgres@db:5432/postgres"</span></span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> agent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">haedlessdev/corecontrol-agent:latest</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> DATABASE_URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"postgresql://postgres:postgres@db:5432/postgres"</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> depends_on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> db</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> condition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">service_healthy</span></span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> db</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres:17</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> restart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">always</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> environment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_USER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_PASSWORD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> POSTGRES_DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> - </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">postgres_data:/var/lib/postgresql/data</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> healthcheck</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> test</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"CMD-SHELL"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"pg_isready -U postgres"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> interval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">2s</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> timeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">2s</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> retries</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">10</span></span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">volumes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> postgres_data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span></code></pre></div><ol start="3"><li>Generate a custom JWT_SECRET with e.g. <a href="https://jwtsecret.com/generate" target="_blank" rel="noreferrer">jwtsecret.com/generate</a></li><li>Start CoreControl with the following command:</li></ol><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker-compose</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> up</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -d</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># OR</span></span>
|
||||||
|
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> compose</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> up</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -d</span></span></code></pre></div><ol start="5"><li>The application is now available at <code>http://localhost:3000</code>.</li></ol><h2 id="authentication" tabindex="-1">Authentication <a class="header-anchor" href="#authentication" aria-label="Permalink to "Authentication""></a></h2><p>CoreControl comes with a default administrator account:</p><ul><li><strong>Email</strong>: <a href="mailto:admin@example.com" target="_blank" rel="noreferrer">admin@example.com</a></li><li><strong>Password</strong>: admin</li></ul><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>For security reasons, it is strongly recommended to change the default credentials immediately after your first login.</p></div><p>You can change the administrator password in the settings after logging in.</p>`,14)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};
|
||||||
1
docs/.vitepress/dist/assets/installation.md.Cz1eOHOr.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Installation","description":"","frontmatter":{},"headers":[],"relativePath":"installation.md","filePath":"installation.md","lastUpdated":1745171698000}'),l={name:"installation.md"};function e(p,s,h,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t("",14)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};
|
||||||
BIN
docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2
vendored
Normal file
1
docs/.vitepress/dist/assets/notifications_Discord.md.C0x5CxmR.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Discord.md.C0x5CxmR.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Email.md.Cugw2BRs.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Email.md.Cugw2BRs.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_General.md.D7AVsSjD.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as o,c as n,o as a,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const _=JSON.parse('{"title":"Notifications","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/General.md","filePath":"notifications/General.md","lastUpdated":1745241280000}'),r={name:"notifications/General.md"};function c(l,e,f,d,p,m){return a(),n("div",null,e[0]||(e[0]=[t("h1",{id:"notifications",tabindex:"-1"},[s("Notifications "),t("a",{class:"header-anchor",href:"#notifications","aria-label":'Permalink to "Notifications"'},"")],-1),t("p",null,"You can set the notifications for CoreControl in the settings. These notifications include when an application goes online or offline and when a server goes online or offline.",-1),t("p",null,[t("img",{src:i,alt:"Notification Settings"})],-1),t("p",null,"You can also customize direct notification texts and improve them with placeholders",-1)]))}const N=o(r,[["render",c]]);export{_ as __pageData,N as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_General.md.D7AVsSjD.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as o,c as n,o as a,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const _=JSON.parse('{"title":"Notifications","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/General.md","filePath":"notifications/General.md","lastUpdated":1745241280000}'),r={name:"notifications/General.md"};function c(l,e,f,d,p,m){return a(),n("div",null,e[0]||(e[0]=[t("h1",{id:"notifications",tabindex:"-1"},[s("Notifications "),t("a",{class:"header-anchor",href:"#notifications","aria-label":'Permalink to "Notifications"'},"")],-1),t("p",null,"You can set the notifications for CoreControl in the settings. These notifications include when an application goes online or offline and when a server goes online or offline.",-1),t("p",null,[t("img",{src:i,alt:"Notification Settings"})],-1),t("p",null,"You can also customize direct notification texts and improve them with placeholders",-1)]))}const N=o(r,[["render",c]]);export{_ as __pageData,N as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Gotify.md.vFHjr6ko.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Gotify.md.vFHjr6ko.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Ntfy.md.CPMnGQVP.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Ntfy.md.CPMnGQVP.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Pushover.md.lZwGAQ0A.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as s,c as a,o,j as e,a as r}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_pushover.CeUzFKPr.png",m=JSON.parse('{"title":"Pushover","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745496781000}'),i={name:"notifications/Pushover.md"};function c(p,t,d,l,u,h){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"pushover",tabindex:"-1"},[r("Pushover "),e("a",{class:"header-anchor",href:"#pushover","aria-label":'Permalink to "Pushover"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Set up"})],-1)]))}const v=s(i,[["render",c]]);export{m as __pageData,v as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Pushover.md.lZwGAQ0A.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as s,c as a,o,j as e,a as r}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_pushover.CeUzFKPr.png",m=JSON.parse('{"title":"Pushover","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745496781000}'),i={name:"notifications/Pushover.md"};function c(p,t,d,l,u,h){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"pushover",tabindex:"-1"},[r("Pushover "),e("a",{class:"header-anchor",href:"#pushover","aria-label":'Permalink to "Pushover"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Set up"})],-1)]))}const v=s(i,[["render",c]]);export{m as __pageData,v as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Telegram.md.B6_EzaEX.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};
|
||||||
1
docs/.vitepress/dist/assets/notifications_Telegram.md.B6_EzaEX.lean.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};
|
||||||
BIN
docs/.vitepress/dist/assets/notifications_discord.BzLLVI_K.png
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |