mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-29 16:14:43 +00:00
Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bd61622e | ||
|
|
127077d929 | ||
|
|
fcb7196d1c | ||
|
|
30aa4bcf57 | ||
|
|
3dc2c6b204 | ||
|
|
65f8e16fbc | ||
|
|
807d4d6ca9 | ||
|
|
70e28cb1c7 | ||
|
|
cba70d5aef | ||
|
|
78c844b1fe | ||
|
|
00a628031e | ||
|
|
a84a985645 | ||
|
|
b08d5b0fef | ||
|
|
58f70ad1d9 | ||
|
|
2f61974f6e | ||
|
|
fde7b3b23e | ||
|
|
677d1c5a58 | ||
|
|
d81d8c04ad | ||
|
|
b368075b12 | ||
|
|
7ad4390db7 | ||
|
|
408b80badf | ||
|
|
8223cc5822 | ||
|
|
c94377c8d5 | ||
|
|
5592a5fd91 | ||
|
|
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 | ||
|
|
676354f53c | ||
|
|
288bb4cc9e | ||
|
|
7b86de86e3 | ||
|
|
3e43c01e5a | ||
|
|
43c09e7a09 | ||
|
|
bdce02ac2b | ||
|
|
eee74c9df9 | ||
|
|
f21dae5646 | ||
|
|
c8a21797c9 | ||
|
|
32d99a09a6 | ||
|
|
7660f8bb5c | ||
|
|
8ff112e86e | ||
|
|
7eafdef288 | ||
|
|
724a634985 | ||
|
|
1e8682646d | ||
|
|
39ba85dcf4 | ||
|
|
a055d72246 | ||
|
|
aef0d6f812 | ||
|
|
b6e17e5d39 | ||
|
|
879ce1e6f1 | ||
|
|
c02b057f8e | ||
|
|
b0d7271d34 | ||
|
|
6ca1b2ac97 | ||
|
|
8059b0e541 | ||
|
|
9c9b556124 | ||
|
|
871ab74576 | ||
|
|
a3b47bd314 | ||
|
|
1088806921 | ||
|
|
a320c04b92 | ||
|
|
e34407539a | ||
|
|
f340543625 | ||
|
|
4aaefe4c55 | ||
|
|
12abe9c0d7 | ||
|
|
2f6957a45d | ||
|
|
ed46598c27 | ||
|
|
1a80c61c34 | ||
|
|
52f3c0432f | ||
|
|
faad5198a6 | ||
|
|
3f1f7b730e | ||
|
|
19ef051e1e | ||
|
|
75d1bd59f4 | ||
|
|
7da6501ca7 | ||
|
|
d779355c4c | ||
|
|
e844712c29 | ||
|
|
36beeb8a2c | ||
|
|
e51600016b | ||
|
|
8fc4fea687 | ||
|
|
0a8ea98dae | ||
|
|
7c86483d48 | ||
|
|
ca31a0b6b3 | ||
|
|
6661b1e711 | ||
|
|
5413dbf948 | ||
|
|
49fec67996 | ||
|
|
a1d9839bcc | ||
|
|
246f6b594c | ||
|
|
7549d8c8c0 | ||
|
|
fdb8b5073f | ||
|
|
07e7a60163 | ||
|
|
460c9103f7 | ||
|
|
14574e370c | ||
|
|
b915e0066d | ||
|
|
489c73ef77 |
@@ -2,4 +2,6 @@ node_modules
|
||||
npm-debug.log
|
||||
.env
|
||||
agent/
|
||||
.next
|
||||
.next
|
||||
docs/
|
||||
screenshots/
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,10 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
|
||||
# Vitepress
|
||||
docs/.vitepress/cache
|
||||
docs/node_modules/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
||||
BIN
ApplicationsFormatted.tsx
Normal file
BIN
ApplicationsFormatted.tsx
Normal file
Binary file not shown.
24
Dockerfile
24
Dockerfile
@@ -4,32 +4,44 @@ FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
COPY ./prisma ./prisma
|
||||
|
||||
# Install all dependencies (including devDependencies)
|
||||
RUN npm install
|
||||
|
||||
# Generate Prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Build the application
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production Stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY --from=builder /app/node_modules/.prisma /app/node_modules/.prisma
|
||||
COPY --from=builder /app/node_modules/@prisma /app/node_modules/@prisma
|
||||
# Copy node_modules from builder
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
# Remove dev dependencies
|
||||
RUN npm prune --production
|
||||
|
||||
# Copy Prisma files
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/next.config.js* ./
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
|
||||
# Run migrations and start
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
||||
|
||||
52
README.md
52
README.md
@@ -6,6 +6,8 @@
|
||||
|
||||
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.
|
||||
|
||||
[](https://www.buymeacoffee.com/corecontrol)
|
||||
|
||||
## Features
|
||||
|
||||
- Dashboard: A clear screen with all the important information about your servers (WIP)
|
||||
@@ -15,26 +17,34 @@ The only dashboard you'll ever need to manage your entire server infrastructure.
|
||||
|
||||
## Screenshots
|
||||
Login Page:
|
||||

|
||||

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

|
||||

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

|
||||

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

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

|
||||

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

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

|
||||

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

|
||||
|
||||
## Roadmap
|
||||
- [ ] Edit Applications, Applications searchbar
|
||||
- [ ] Customizable Dashboard
|
||||
- [ ] Notifications
|
||||
- [ ] Uptime History
|
||||
- [ ] Simple Server Monitoring
|
||||
- [X] Edit Applications, Applications searchbar
|
||||
- [X] Uptime History
|
||||
- [X] Notifications
|
||||
- [X] Simple Server Monitoring
|
||||
- [ ] Improved Network Flowchart with custom elements (like Network switches)
|
||||
- [ ] Advanced Settings (Disable Uptime Tracking & more)
|
||||
|
||||
@@ -48,19 +58,16 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
LOGIN_EMAIL: "mail@example.com"
|
||||
LOGIN_PASSWORD: "SecretPassword"
|
||||
JWT_SECRET: RANDOM_SECRET
|
||||
ACCOUNT_SECRET: RANDOM_SECRET
|
||||
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||
depends_on:
|
||||
- db
|
||||
- agent
|
||||
|
||||
agent:
|
||||
image: haedlessdev/corecontrol-agent:latest
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:17
|
||||
@@ -71,11 +78,20 @@ services:
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
#### Default Login
|
||||
__E-Mail:__ admin@example.com\
|
||||
__Password:__ admin
|
||||
|
||||
## Tech Stack & Credits
|
||||
|
||||
The application is build with:
|
||||
@@ -85,6 +101,8 @@ The application is build with:
|
||||
- PostgreSQL with [Prisma ORM](https://www.prisma.io/)
|
||||
- Icons by [Lucide](https://lucide.dev/)
|
||||
- Flowcharts by [React Flow](https://reactflow.dev/)
|
||||
- Application icons by [selfh.st/icons](selfh.st/icons)
|
||||
- Monitoring Tool by [Glances](https://github.com/nicolargo/glances)
|
||||
- and a lot of love ❤️
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -1 +1 @@
|
||||
.env.local
|
||||
.env
|
||||
@@ -1,25 +1,25 @@
|
||||
# --- Build Stage ---
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.19-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app .
|
||||
|
||||
# --- Run Stage ---
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/app .
|
||||
|
||||
CMD ["./app"]
|
||||
WORKDIR /app
|
||||
|
||||
ENV GO111MODULE=on
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app ./cmd/agent
|
||||
|
||||
# --- Run Stage ---
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/app .
|
||||
|
||||
CMD ["./app"]
|
||||
|
||||
111
agent/cmd/agent/main.go
Normal file
111
agent/cmd/agent/main.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/app"
|
||||
"github.com/corecontrol/agent/internal/database"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
"github.com/corecontrol/agent/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize database
|
||||
db, err := database.InitDB()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Database initialization failed: %v\n", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Initialize notification sender
|
||||
notifSender := notifications.NewNotificationSender()
|
||||
|
||||
// Initial load of notifications
|
||||
notifs, err := database.LoadNotifications(db)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load notifications: %v", err))
|
||||
}
|
||||
notifSender.UpdateNotifications(notifs)
|
||||
|
||||
// Reload notification configs every minute
|
||||
go func() {
|
||||
reloadTicker := time.NewTicker(time.Minute)
|
||||
defer reloadTicker.Stop()
|
||||
|
||||
for range reloadTicker.C {
|
||||
newNotifs, err := database.LoadNotifications(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to reload notifications: %v\n", err)
|
||||
continue
|
||||
}
|
||||
notifSender.UpdateNotifications(newNotifs)
|
||||
fmt.Println("Reloaded notification configurations")
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean up old entries hourly
|
||||
go func() {
|
||||
deletionTicker := time.NewTicker(time.Hour)
|
||||
defer deletionTicker.Stop()
|
||||
|
||||
for range deletionTicker.C {
|
||||
if err := database.DeleteOldEntries(db); err != nil {
|
||||
fmt.Printf("Error deleting old entries: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for test notifications every 10 seconds
|
||||
go func() {
|
||||
testNotifTicker := time.NewTicker(10 * time.Second)
|
||||
defer testNotifTicker.Stop()
|
||||
|
||||
for range testNotifTicker.C {
|
||||
notifs := notifSender.GetNotifications()
|
||||
database.CheckAndSendTestNotifications(db, notifs, notifSender.SendSpecificNotification)
|
||||
}
|
||||
}()
|
||||
|
||||
// HTTP clients
|
||||
appClient := &http.Client{
|
||||
Timeout: 4 * time.Second,
|
||||
}
|
||||
|
||||
serverClient := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Server monitoring every 5 seconds
|
||||
go func() {
|
||||
serverTicker := time.NewTicker(5 * time.Second)
|
||||
defer serverTicker.Stop()
|
||||
|
||||
for range serverTicker.C {
|
||||
servers, err := database.GetServers(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting servers: %v\n", err)
|
||||
continue
|
||||
}
|
||||
server.MonitorServers(db, serverClient, servers, notifSender)
|
||||
}
|
||||
}()
|
||||
|
||||
// Application monitoring every 10 seconds
|
||||
appTicker := time.NewTicker(time.Second)
|
||||
defer appTicker.Stop()
|
||||
|
||||
for now := range appTicker.C {
|
||||
if now.Second()%10 != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
apps, err := database.GetApplications(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting applications: %v\n", err)
|
||||
continue
|
||||
}
|
||||
app.MonitorApplications(db, appClient, apps, notifSender)
|
||||
}
|
||||
}
|
||||
16
agent/go.mod
16
agent/go.mod
@@ -1,20 +1,22 @@
|
||||
module agent
|
||||
module github.com/corecontrol/agent
|
||||
|
||||
go 1.24.1
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v4 v4.18.3
|
||||
github.com/jackc/pgx/v4 v4.18.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgconn v1.14.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
golang.org/x/crypto v0.20.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
)
|
||||
|
||||
43
agent/go.sum
43
agent/go.sum
@@ -25,8 +25,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
|
||||
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
@@ -42,8 +42,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
@@ -57,11 +57,12 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
|
||||
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -98,13 +99,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -126,17 +132,22 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -149,15 +160,21 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -165,15 +182,21 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/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=
|
||||
|
||||
130
agent/internal/app/monitor.go
Normal file
130
agent/internal/app/monitor.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
)
|
||||
|
||||
// MonitorApplications checks and updates the status of all applications
|
||||
func MonitorApplications(db *sql.DB, client *http.Client, apps []models.Application, notifSender *notifications.NotificationSender) {
|
||||
var notificationTemplate string
|
||||
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||
if err != nil || notificationTemplate == "" {
|
||||
notificationTemplate = "The application !name (!url) went !status!"
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
|
||||
fmt.Printf("%s Checking...\n", logPrefix)
|
||||
|
||||
// Determine which URL to use for monitoring
|
||||
checkURL := app.PublicURL
|
||||
if app.UptimeCheckURL != "" {
|
||||
checkURL = app.UptimeCheckURL
|
||||
fmt.Printf("%s Using custom uptime check URL: %s\n", logPrefix, checkURL)
|
||||
}
|
||||
|
||||
parsedURL, parseErr := url.Parse(checkURL)
|
||||
if parseErr != nil {
|
||||
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
|
||||
continue
|
||||
}
|
||||
|
||||
hostIsIP := isIPAddress(parsedURL.Hostname())
|
||||
var isOnline bool
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", checkURL, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode)
|
||||
} else {
|
||||
fmt.Printf("%s Connection error: %v\n", logPrefix, err)
|
||||
|
||||
if hostIsIP {
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
var certErr x509.HostnameError
|
||||
var unknownAuthErr x509.UnknownAuthorityError
|
||||
if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) {
|
||||
fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix)
|
||||
isOnline = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isOnline != app.Online {
|
||||
status := "offline"
|
||||
if isOnline {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(notificationTemplate, "!name", app.Name)
|
||||
message = strings.ReplaceAll(message, "!url", app.PublicURL)
|
||||
message = strings.ReplaceAll(message, "!status", status)
|
||||
|
||||
notifSender.SendNotifications(message)
|
||||
}
|
||||
|
||||
// Update application status in database
|
||||
updateApplicationStatus(db, app.ID, isOnline)
|
||||
|
||||
// Add entry to uptime history
|
||||
addUptimeHistoryEntry(db, app.ID, isOnline)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update application status
|
||||
func updateApplicationStatus(db *sql.DB, appID int, online bool) {
|
||||
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer dbCancel()
|
||||
|
||||
_, err := db.ExecContext(dbCtx,
|
||||
`UPDATE application SET online = $1 WHERE id = $2`,
|
||||
online, appID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("DB update failed for app ID %d: %v\n", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add uptime history entry
|
||||
func addUptimeHistoryEntry(db *sql.DB, appID int, online bool) {
|
||||
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer dbCancel()
|
||||
|
||||
_, err := db.ExecContext(dbCtx,
|
||||
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
|
||||
appID, online,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("History insert failed for app ID %d: %v\n", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a host is an IP address
|
||||
func isIPAddress(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil
|
||||
}
|
||||
198
agent/internal/database/database.go
Normal file
198
agent/internal/database/database.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// InitDB initializes the database connection
|
||||
func InitDB() (*sql.DB, error) {
|
||||
// Load environment variables
|
||||
if err := godotenv.Load(); err != nil {
|
||||
fmt.Println("No env vars found")
|
||||
}
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
return nil, fmt.Errorf("DATABASE_URL not set")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", dbURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database connection failed: %v", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// GetApplications fetches all applications with public URLs
|
||||
func GetApplications(db *sql.DB) ([]models.Application, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, name, "publicURL", online, "uptimeCheckURL" FROM application WHERE "publicURL" IS NOT NULL`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching applications: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []models.Application
|
||||
for rows.Next() {
|
||||
var app models.Application
|
||||
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online, &app.UptimeCheckURL); err != nil {
|
||||
fmt.Printf("Error scanning row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// GetServers fetches all servers with monitoring enabled
|
||||
func GetServers(db *sql.DB) ([]models.Server, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
|
||||
FROM server WHERE monitoring = true`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching servers: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var servers []models.Server
|
||||
for rows.Next() {
|
||||
var server models.Server
|
||||
if err := rows.Scan(
|
||||
&server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL,
|
||||
&server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage,
|
||||
); err != nil {
|
||||
fmt.Printf("Error scanning server row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// LoadNotifications loads all enabled notifications
|
||||
func LoadNotifications(db *sql.DB) ([]models.Notification, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
|
||||
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken",
|
||||
"pushoverUrl", "pushoverToken", "pushoverUser"
|
||||
FROM notification
|
||||
WHERE enabled = true`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var configs []models.Notification
|
||||
for rows.Next() {
|
||||
var n models.Notification
|
||||
if err := rows.Scan(
|
||||
&n.ID, &n.Enabled, &n.Type,
|
||||
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo,
|
||||
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook,
|
||||
&n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
|
||||
&n.PushoverUrl, &n.PushoverToken, &n.PushoverUser,
|
||||
); err != nil {
|
||||
fmt.Printf("Error scanning notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
configs = append(configs, n)
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// DeleteOldEntries removes entries older than 30 days
|
||||
func DeleteOldEntries(db *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Delete old uptime history entries
|
||||
res, err := db.ExecContext(ctx,
|
||||
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
|
||||
|
||||
// Delete old server history entries
|
||||
res, err = db.ExecContext(ctx,
|
||||
`DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, _ = res.RowsAffected()
|
||||
fmt.Printf("Deleted %d old entries from server_history\n", affected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateServerStatus updates a server's status and metrics
|
||||
func UpdateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.ExecContext(ctx,
|
||||
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5
|
||||
WHERE id = $6`,
|
||||
online, cpuUsage, ramUsage, diskUsage, uptime, serverID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckAndSendTestNotifications checks for and processes test notifications
|
||||
func CheckAndSendTestNotifications(db *sql.DB, notifications []models.Notification, sendFunc func(models.Notification, string)) {
|
||||
// Query for test notifications
|
||||
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching test notifications: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process each test notification
|
||||
var testIds []int
|
||||
for rows.Next() {
|
||||
var id, notificationId int
|
||||
if err := rows.Scan(&id, ¬ificationId); err != nil {
|
||||
fmt.Printf("Error scanning test notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to list of IDs to delete
|
||||
testIds = append(testIds, id)
|
||||
|
||||
// Find the notification configuration
|
||||
for _, n := range notifications {
|
||||
if n.ID == notificationId {
|
||||
// Send test notification
|
||||
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
|
||||
sendFunc(n, "Test notification from CoreControl")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete processed test notifications
|
||||
if len(testIds) > 0 {
|
||||
for _, id := range testIds {
|
||||
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
agent/internal/models/models.go
Normal file
75
agent/internal/models/models.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
ID int
|
||||
Name string
|
||||
PublicURL string
|
||||
Online bool
|
||||
UptimeCheckURL string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ID int
|
||||
Name string
|
||||
Monitoring bool
|
||||
MonitoringURL sql.NullString
|
||||
Online bool
|
||||
CpuUsage sql.NullFloat64
|
||||
RamUsage sql.NullFloat64
|
||||
DiskUsage sql.NullFloat64
|
||||
Uptime sql.NullString
|
||||
}
|
||||
|
||||
type CPUResponse struct {
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
type MemoryResponse struct {
|
||||
Active int64 `json:"active"`
|
||||
Available int64 `json:"available"`
|
||||
Buffers int64 `json:"buffers"`
|
||||
Cached int64 `json:"cached"`
|
||||
Free int64 `json:"free"`
|
||||
Inactive int64 `json:"inactive"`
|
||||
Percent float64 `json:"percent"`
|
||||
Shared int64 `json:"shared"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type FSResponse []struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
MntPoint string `json:"mnt_point"`
|
||||
Percent float64 `json:"percent"`
|
||||
}
|
||||
|
||||
type UptimeResponse struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int
|
||||
Enabled bool
|
||||
Type string
|
||||
SMTPHost sql.NullString
|
||||
SMTPPort sql.NullInt64
|
||||
SMTPFrom sql.NullString
|
||||
SMTPUser sql.NullString
|
||||
SMTPPass sql.NullString
|
||||
SMTPSecure sql.NullBool
|
||||
SMTPTo sql.NullString
|
||||
TelegramChatID sql.NullString
|
||||
TelegramToken sql.NullString
|
||||
DiscordWebhook sql.NullString
|
||||
GotifyUrl sql.NullString
|
||||
GotifyToken sql.NullString
|
||||
NtfyUrl sql.NullString
|
||||
NtfyToken sql.NullString
|
||||
PushoverUrl sql.NullString
|
||||
PushoverToken sql.NullString
|
||||
PushoverUser sql.NullString
|
||||
}
|
||||
239
agent/internal/notifications/notifications.go
Normal file
239
agent/internal/notifications/notifications.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
type NotificationSender struct {
|
||||
notifications []models.Notification
|
||||
notifMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewNotificationSender creates a new notification sender
|
||||
func NewNotificationSender() *NotificationSender {
|
||||
return &NotificationSender{
|
||||
notifications: []models.Notification{},
|
||||
notifMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNotifications updates the stored notifications
|
||||
func (ns *NotificationSender) UpdateNotifications(notifs []models.Notification) {
|
||||
ns.notifMutex.Lock()
|
||||
defer ns.notifMutex.Unlock()
|
||||
|
||||
copyDst := make([]models.Notification, len(notifs))
|
||||
copy(copyDst, notifs)
|
||||
ns.notifications = copyDst
|
||||
}
|
||||
|
||||
// GetNotifications returns a safe copy of current notifications
|
||||
func (ns *NotificationSender) GetNotifications() []models.Notification {
|
||||
ns.notifMutex.RLock()
|
||||
defer ns.notifMutex.RUnlock()
|
||||
|
||||
copyDst := make([]models.Notification, len(ns.notifications))
|
||||
copy(copyDst, ns.notifications)
|
||||
return copyDst
|
||||
}
|
||||
|
||||
// SendNotifications sends a message to all configured notifications
|
||||
func (ns *NotificationSender) SendNotifications(message string) {
|
||||
notifs := ns.GetNotifications()
|
||||
|
||||
for _, n := range notifs {
|
||||
ns.SendSpecificNotification(n, message)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSpecificNotification sends a message to a specific notification
|
||||
func (ns *NotificationSender) SendSpecificNotification(n models.Notification, message string) {
|
||||
fmt.Println("Sending specific notification..." + n.Type)
|
||||
switch n.Type {
|
||||
case "smtp":
|
||||
if n.SMTPHost.Valid && n.SMTPTo.Valid {
|
||||
ns.sendEmail(n, message)
|
||||
}
|
||||
case "telegram":
|
||||
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
|
||||
ns.sendTelegram(n, message)
|
||||
}
|
||||
case "discord":
|
||||
if n.DiscordWebhook.Valid {
|
||||
ns.sendDiscord(n, message)
|
||||
}
|
||||
case "gotify":
|
||||
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
|
||||
ns.sendGotify(n, message)
|
||||
}
|
||||
case "ntfy":
|
||||
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
|
||||
ns.sendNtfy(n, message)
|
||||
}
|
||||
case "pushover":
|
||||
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
|
||||
ns.sendPushover(n, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a host is an IP address
|
||||
func (ns *NotificationSender) isIPAddress(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// Individual notification methods
|
||||
func (ns *NotificationSender) sendEmail(n models.Notification, body string) {
|
||||
// Initialize SMTP dialer with host, port, user, pass
|
||||
d := gomail.NewDialer(
|
||||
n.SMTPHost.String,
|
||||
int(n.SMTPPort.Int64),
|
||||
n.SMTPUser.String,
|
||||
n.SMTPPass.String,
|
||||
)
|
||||
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
|
||||
d.SSL = true
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", n.SMTPFrom.String)
|
||||
m.SetHeader("To", n.SMTPTo.String)
|
||||
m.SetHeader("Subject", "Uptime Notification")
|
||||
m.SetBody("text/plain", body)
|
||||
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
fmt.Printf("Email send failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendTelegram(n models.Notification, message string) {
|
||||
apiUrl := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
|
||||
n.TelegramToken.String,
|
||||
n.TelegramChatID.String,
|
||||
message,
|
||||
)
|
||||
resp, err := http.Get(apiUrl)
|
||||
if err != nil {
|
||||
fmt.Printf("Telegram send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendDiscord(n models.Notification, message string) {
|
||||
payload := fmt.Sprintf(`{"content": "%s"}`, message)
|
||||
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
fmt.Printf("Discord request creation failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Discord send failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendGotify(n models.Notification, message string) {
|
||||
baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/")
|
||||
targetURL := fmt.Sprintf("%s/message", baseURL)
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("message", message)
|
||||
form.Add("priority", "5")
|
||||
|
||||
req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
fmt.Printf("Gotify: ERROR creating request: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-Gotify-Key", n.GotifyToken.String)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Gotify: ERROR sending request: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendNtfy(n models.Notification, message string) {
|
||||
fmt.Println("Sending Ntfy notification...")
|
||||
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
|
||||
|
||||
// Don't append a topic to the URL - the URL itself should have the correct endpoint
|
||||
requestURL := baseURL
|
||||
|
||||
// Send message directly as request body instead of JSON
|
||||
req, err := http.NewRequest("POST", requestURL, strings.NewReader(message))
|
||||
if err != nil {
|
||||
fmt.Printf("Ntfy: ERROR creating request: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if n.NtfyToken.Valid {
|
||||
req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String)
|
||||
}
|
||||
// Use text/plain instead of application/json
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Ntfy: ERROR sending request: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendPushover(n models.Notification, message string) {
|
||||
form := url.Values{}
|
||||
form.Add("token", n.PushoverToken.String)
|
||||
form.Add("user", n.PushoverUser.String)
|
||||
form.Add("message", message)
|
||||
|
||||
req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
fmt.Printf("Pushover: ERROR creating request: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Pushover: ERROR sending request: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
291
agent/internal/server/monitor.go
Normal file
291
agent/internal/server/monitor.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
)
|
||||
|
||||
// MonitorServers checks and updates the status of all servers
|
||||
func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, notifSender *notifications.NotificationSender) {
|
||||
var notificationTemplate string
|
||||
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate)
|
||||
if err != nil || notificationTemplate == "" {
|
||||
notificationTemplate = "The server !name is now !status!"
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
if !server.Monitoring || !server.MonitoringURL.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
logPrefix := fmt.Sprintf("[Server %s]", server.Name)
|
||||
fmt.Printf("%s Checking...\n", logPrefix)
|
||||
|
||||
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
|
||||
var cpuUsage, ramUsage, diskUsage float64
|
||||
var online = true
|
||||
var uptimeStr string
|
||||
|
||||
// Get CPU usage
|
||||
online, cpuUsage = fetchCPUUsage(client, baseURL, logPrefix)
|
||||
if !online {
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get uptime if server is online
|
||||
uptimeStr = fetchUptime(client, baseURL, logPrefix)
|
||||
|
||||
// Get Memory usage
|
||||
memOnline, memUsage := fetchMemoryUsage(client, baseURL, logPrefix)
|
||||
if !memOnline {
|
||||
online = false
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
ramUsage = memUsage
|
||||
|
||||
// Get Disk usage
|
||||
diskOnline, diskUsageVal := fetchDiskUsage(client, baseURL, logPrefix)
|
||||
if !diskOnline {
|
||||
online = false
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, "")
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
diskUsage = diskUsageVal
|
||||
|
||||
// Check if status changed and send notification if needed
|
||||
if online != server.Online {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
|
||||
// Update server status with metrics
|
||||
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
|
||||
// Add entry to server history
|
||||
addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
|
||||
|
||||
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n",
|
||||
logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch CPU usage
|
||||
func fetchCPUUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer cpuResp.Body.Close()
|
||||
|
||||
if cpuResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var cpuData models.CPUResponse
|
||||
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
|
||||
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, cpuData.Total
|
||||
}
|
||||
|
||||
// Helper function to fetch memory usage
|
||||
func fetchMemoryUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer memResp.Body.Close()
|
||||
|
||||
if memResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var memData models.MemoryResponse
|
||||
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
|
||||
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, memData.Percent
|
||||
}
|
||||
|
||||
// Helper function to fetch disk usage
|
||||
func fetchDiskUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
defer fsResp.Body.Close()
|
||||
|
||||
if fsResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
var fsData models.FSResponse
|
||||
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
|
||||
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if len(fsData) > 0 {
|
||||
return true, fsData[0].Percent
|
||||
}
|
||||
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// Helper function to fetch uptime
|
||||
func fetchUptime(client *http.Client, baseURL, logPrefix string) string {
|
||||
uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL))
|
||||
if err != nil || uptimeResp.StatusCode != http.StatusOK {
|
||||
if err != nil {
|
||||
fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err)
|
||||
} else {
|
||||
fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode)
|
||||
uptimeResp.Body.Close()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
defer uptimeResp.Body.Close()
|
||||
|
||||
// Read the response body as a string first
|
||||
uptimeBytes, err := io.ReadAll(uptimeResp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
uptimeStr := strings.Trim(string(uptimeBytes), "\"")
|
||||
|
||||
// Try to parse as JSON object first, then fallback to direct string if that fails
|
||||
var uptimeData models.UptimeResponse
|
||||
if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" {
|
||||
uptimeStr = formatUptime(uptimeData.Value)
|
||||
} else {
|
||||
// Use the string directly
|
||||
uptimeStr = formatUptime(uptimeStr)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr)
|
||||
return uptimeStr
|
||||
}
|
||||
|
||||
// Helper function to send notification about status change
|
||||
func sendStatusChangeNotification(server models.Server, online bool, template string, notifSender *notifications.NotificationSender) {
|
||||
status := "offline"
|
||||
if online {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(template, "!name", server.Name)
|
||||
message = strings.ReplaceAll(message, "!status", status)
|
||||
|
||||
notifSender.SendNotifications(message)
|
||||
}
|
||||
|
||||
// Helper function to update server status
|
||||
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.ExecContext(ctx,
|
||||
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5
|
||||
WHERE id = $6`,
|
||||
online, cpuUsage, ramUsage, diskUsage, uptime, serverID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add server history entry
|
||||
func addServerHistoryEntry(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.ExecContext(ctx,
|
||||
`INSERT INTO server_history(
|
||||
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt"
|
||||
) VALUES ($1, $2, $3, $4, $5, now())`,
|
||||
serverID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to insert server history (ID: %d): %v\n", serverID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// FormatUptime formats the uptime string to a standard format
|
||||
func formatUptime(uptimeStr string) string {
|
||||
// Example input: "3 days, 3:52:36"
|
||||
// Target output: "28.6 13:52"
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Parse the uptime components
|
||||
parts := strings.Split(uptimeStr, ", ")
|
||||
|
||||
var days int
|
||||
var timeStr string
|
||||
|
||||
if len(parts) == 2 {
|
||||
// Has days part and time part
|
||||
_, err := fmt.Sscanf(parts[0], "%d days", &days)
|
||||
if err != nil {
|
||||
// Try singular "day"
|
||||
_, err = fmt.Sscanf(parts[0], "%d day", &days)
|
||||
if err != nil {
|
||||
return uptimeStr // Return original if parsing fails
|
||||
}
|
||||
}
|
||||
timeStr = parts[1]
|
||||
} else if len(parts) == 1 {
|
||||
// Only has time part (less than a day)
|
||||
days = 0
|
||||
timeStr = parts[0]
|
||||
} else {
|
||||
return uptimeStr // Return original if format is unexpected
|
||||
}
|
||||
|
||||
// Parse the time component (hours:minutes:seconds)
|
||||
var hours, minutes, seconds int
|
||||
_, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds)
|
||||
if err != nil {
|
||||
return uptimeStr // Return original if parsing fails
|
||||
}
|
||||
|
||||
// Calculate the total duration
|
||||
duration := time.Duration(days)*24*time.Hour +
|
||||
time.Duration(hours)*time.Hour +
|
||||
time.Duration(minutes)*time.Minute +
|
||||
time.Duration(seconds)*time.Second
|
||||
|
||||
// Calculate the start time by subtracting the duration from now
|
||||
startTime := now.Add(-duration)
|
||||
|
||||
// Format the result in the required format (day.month hour:minute)
|
||||
return startTime.Format("2.1 15:04")
|
||||
}
|
||||
101
agent/main.go
101
agent/main.go
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
ID int
|
||||
PublicURL string
|
||||
Online bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
fmt.Println("No env vars found")
|
||||
}
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
panic("DATABASE_URL not set")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", dbURL)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Database connection failed: %v\n", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 4 * time.Second,
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
apps := getApplications(db)
|
||||
checkAndUpdateStatus(db, client, apps)
|
||||
}
|
||||
}
|
||||
|
||||
func getApplications(db *sql.DB) []Application {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, "publicURL", online
|
||||
FROM application
|
||||
WHERE "publicURL" IS NOT NULL
|
||||
`)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching applications: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []Application
|
||||
for rows.Next() {
|
||||
var app Application
|
||||
err := rows.Scan(&app.ID, &app.PublicURL, &app.Online)
|
||||
if err != nil {
|
||||
fmt.Printf("Error scanning row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
|
||||
for _, app := range apps {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", app.PublicURL, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating request: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
isOnline := false
|
||||
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
isOnline = true
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx,
|
||||
"UPDATE application SET online = $1 WHERE id = $2",
|
||||
isOnline,
|
||||
app.ID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Update failed for app %d: %v\n", app.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@ interface AddRequest {
|
||||
icon: string;
|
||||
publicURL: string;
|
||||
localURL: string;
|
||||
uptimecheckUrl: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: AddRequest = await request.json();
|
||||
const { serverId, name, description, icon, publicURL, localURL } = body;
|
||||
const { serverId, name, description, icon, publicURL, localURL, uptimecheckUrl } = body;
|
||||
|
||||
const application = await prisma.application.create({
|
||||
data: {
|
||||
@@ -22,7 +23,8 @@ export async function POST(request: NextRequest) {
|
||||
description,
|
||||
icon,
|
||||
publicURL,
|
||||
localURL
|
||||
localURL,
|
||||
uptimecheckUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export async function POST(request: NextRequest) {
|
||||
where: { id: id }
|
||||
});
|
||||
|
||||
await prisma.uptime_history.deleteMany({
|
||||
where: { applicationId: id }
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
@@ -9,12 +9,13 @@ interface EditRequest {
|
||||
icon: string;
|
||||
publicURL: string;
|
||||
localURL: string;
|
||||
uptimecheckUrl: string;
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body: EditRequest = await request.json();
|
||||
const { id, name, description, serverId, icon, publicURL, localURL } = body;
|
||||
const { id, name, description, serverId, icon, publicURL, localURL, uptimecheckUrl } = body;
|
||||
|
||||
const existingApp = await prisma.application.findUnique({ where: { id } });
|
||||
if (!existingApp) {
|
||||
@@ -29,7 +30,8 @@ export async function PUT(request: NextRequest) {
|
||||
description,
|
||||
icon,
|
||||
publicURL,
|
||||
localURL
|
||||
localURL,
|
||||
uptimecheckUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
applications: applicationsWithServers,
|
||||
servers: servers_all,
|
||||
maxPage
|
||||
maxPage,
|
||||
totalItems: totalCount
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
@@ -23,7 +23,23 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const searchResults = fuse.search(searchterm);
|
||||
|
||||
const results = searchResults.map(({ item }) => item);
|
||||
const searchedApps = searchResults.map(({ item }) => item);
|
||||
|
||||
// Get server IDs from the search results
|
||||
const serverIds = searchedApps
|
||||
.map(app => app.serverId)
|
||||
.filter((id): id is number => id !== null);
|
||||
|
||||
// Fetch server data for these applications
|
||||
const servers = await prisma.server.findMany({
|
||||
where: { id: { in: serverIds } }
|
||||
});
|
||||
|
||||
// Add server name to each application
|
||||
const results = searchedApps.map(app => ({
|
||||
...app,
|
||||
server: servers.find(s => s.id === app.serverId)?.name || "No server"
|
||||
}));
|
||||
|
||||
return NextResponse.json({ results });
|
||||
} catch (error: any) {
|
||||
|
||||
175
app/api/applications/uptime/route.ts
Normal file
175
app/api/applications/uptime/route.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface RequestBody {
|
||||
timespan?: number;
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
}
|
||||
|
||||
|
||||
const getTimeRange = (timespan: number) => {
|
||||
const now = new Date();
|
||||
switch (timespan) {
|
||||
case 1:
|
||||
return {
|
||||
start: new Date(now.getTime() - 60 * 60 * 1000),
|
||||
interval: 'minute'
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
start: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||
interval: 'hour'
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
|
||||
interval: 'day'
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
|
||||
interval: 'day'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
start: new Date(now.getTime() - 60 * 60 * 1000),
|
||||
interval: 'minute'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const generateIntervals = (timespan: number) => {
|
||||
const now = new Date();
|
||||
now.setSeconds(0, 0);
|
||||
|
||||
switch (timespan) {
|
||||
case 1: // 1 hour - 60 one-minute intervals
|
||||
return Array.from({ length: 60 }, (_, i) => {
|
||||
const d = new Date(now);
|
||||
d.setMinutes(d.getMinutes() - i);
|
||||
d.setSeconds(0, 0);
|
||||
return d;
|
||||
});
|
||||
|
||||
case 2: // 1 day - 24 one-hour intervals
|
||||
return Array.from({ length: 24 }, (_, i) => {
|
||||
const d = new Date(now);
|
||||
d.setHours(d.getHours() - i);
|
||||
d.setMinutes(0, 0, 0);
|
||||
return d;
|
||||
});
|
||||
|
||||
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) => {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
});
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getIntervalKey = (date: Date, timespan: number) => {
|
||||
const d = new Date(date);
|
||||
switch (timespan) {
|
||||
case 1: // 1 hour - minute intervals
|
||||
d.setSeconds(0, 0);
|
||||
return d.toISOString();
|
||||
case 2: // 1 day - hour intervals
|
||||
d.setMinutes(0, 0, 0);
|
||||
return d.toISOString();
|
||||
case 3: // 7 days - day intervals
|
||||
case 4: // 30 days - day intervals
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d.toISOString();
|
||||
default:
|
||||
return d.toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json();
|
||||
const skip = (page - 1) * itemsPerPage;
|
||||
|
||||
// Get paginated and sorted applications
|
||||
const [applications, totalCount] = await Promise.all([
|
||||
prisma.application.findMany({
|
||||
skip,
|
||||
take: itemsPerPage,
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
prisma.application.count()
|
||||
]);
|
||||
|
||||
const applicationIds = applications.map(app => app.id);
|
||||
|
||||
// Get time range and intervals
|
||||
const { start } = getTimeRange(timespan);
|
||||
const intervals = generateIntervals(timespan);
|
||||
|
||||
// Get uptime history for the filtered applications
|
||||
const uptimeHistory = await prisma.uptime_history.findMany({
|
||||
where: {
|
||||
applicationId: { in: applicationIds },
|
||||
createdAt: { gte: start }
|
||||
},
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
|
||||
// Process data for each application
|
||||
const result = applications.map(app => {
|
||||
const appChecks = uptimeHistory.filter(check => check.applicationId === app.id);
|
||||
const checksMap = new Map<string, { failed: number; total: number }>();
|
||||
|
||||
for (const check of appChecks) {
|
||||
const intervalKey = getIntervalKey(check.createdAt, timespan);
|
||||
const current = checksMap.get(intervalKey) || { failed: 0, total: 0 };
|
||||
current.total++;
|
||||
if (!check.online) current.failed++;
|
||||
checksMap.set(intervalKey, current);
|
||||
}
|
||||
|
||||
const uptimeSummary = intervals.map(interval => {
|
||||
const intervalKey = getIntervalKey(interval, timespan);
|
||||
const stats = checksMap.get(intervalKey);
|
||||
|
||||
return {
|
||||
timestamp: intervalKey,
|
||||
missing: !stats,
|
||||
online: stats ? (stats.failed / stats.total) <= 0.5 : null
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
appName: app.name,
|
||||
appId: app.id,
|
||||
uptimeSummary
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
data: result,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(totalCount / itemsPerPage),
|
||||
totalItems: totalCount
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
56
app/api/auth/edit_email/route.ts
Normal file
56
app/api/auth/edit_email/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface EditEmailRequest {
|
||||
newEmail: string;
|
||||
jwtToken: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: EditEmailRequest = await request.json();
|
||||
const { newEmail, jwtToken } = body;
|
||||
|
||||
// Ensure JWT_SECRET is defined
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
|
||||
if (!decoded.account_secret) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the user by account id
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.account_secret },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
|
||||
// Check if the new email is already in use
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: newEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json({ error: 'Email already in use' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Update the user's email
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { email: newEmail },
|
||||
});
|
||||
|
||||
|
||||
return NextResponse.json({ message: 'Email updated successfully' });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
55
app/api/auth/edit_password/route.ts
Normal file
55
app/api/auth/edit_password/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
interface EditEmailRequest {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
jwtToken: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: EditEmailRequest = await request.json();
|
||||
const { oldPassword, newPassword, jwtToken } = body;
|
||||
|
||||
// Ensure JWT_SECRET is defined
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string };
|
||||
if (!decoded.account_secret) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the user by account id
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.account_secret },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if the old password is correct
|
||||
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isOldPasswordValid) {
|
||||
return NextResponse.json({ error: 'Old password is incorrect' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
// Update the user's password
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { password: hashedNewPassword },
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: 'Password updated successfully' });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
@@ -11,17 +13,50 @@ export async function POST(request: NextRequest) {
|
||||
const body: LoginRequest = await request.json();
|
||||
const { username, password } = body;
|
||||
|
||||
if(username !== process.env.LOGIN_EMAIL || password !== process.env.LOGIN_PASSWORD) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
// Ensure JWT_SECRET is defined
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
}
|
||||
|
||||
|
||||
let accountId: string = '';
|
||||
// Check if there are any entries in user
|
||||
const userCount = await prisma.user.count();
|
||||
if (userCount === 0) {
|
||||
if(username=== "admin@example.com" && password === "admin") {
|
||||
// Hash the password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
// Create the first user with hashed password
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: username,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the account id
|
||||
accountId = user.id;
|
||||
} else {
|
||||
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
|
||||
}
|
||||
} else {
|
||||
// Get the user by username
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: username },
|
||||
});
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
|
||||
}
|
||||
// Check if the password is correct
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json({ error: "Wrong credentials" }, { status: 401 });
|
||||
}
|
||||
// Get the account id
|
||||
accountId = user.id;
|
||||
}
|
||||
|
||||
// Create JWT
|
||||
const token = jwt.sign({ account_secret: process.env.ACCOUNT_SECRET }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
||||
const token = jwt.sign({ account_secret: accountId }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
||||
|
||||
return NextResponse.json({ token });
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface ValidateRequest {
|
||||
token: string;
|
||||
@@ -16,6 +16,14 @@ export async function POST(request: NextRequest) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
}
|
||||
|
||||
// Get the account id
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {},
|
||||
});
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload & { id: string };
|
||||
|
||||
@@ -23,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
|
||||
}
|
||||
|
||||
if(decoded.account_secret !== process.env.ACCOUNT_SECRET) {
|
||||
if(decoded.account_secret !== user.id) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,19 @@ import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const serverCount = await prisma.server.count();
|
||||
const serverCountNoVMs = await prisma.server.count({
|
||||
where: {
|
||||
hostServer: 0
|
||||
}
|
||||
});
|
||||
|
||||
const serverCountOnlyVMs = await prisma.server.count({
|
||||
where: {
|
||||
hostServer: {
|
||||
not: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const applicationCount = await prisma.application.count();
|
||||
|
||||
@@ -12,7 +24,8 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
serverCount,
|
||||
serverCountNoVMs,
|
||||
serverCountOnlyVMs,
|
||||
applicationCount,
|
||||
onlineApplicationsCount
|
||||
});
|
||||
|
||||
@@ -10,6 +10,9 @@ interface Node {
|
||||
};
|
||||
position: { x: number; y: number };
|
||||
style: React.CSSProperties;
|
||||
draggable?: boolean;
|
||||
selectable?: boolean;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
@@ -27,6 +30,8 @@ interface Server {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
host: boolean;
|
||||
hostServer: number | null;
|
||||
}
|
||||
|
||||
interface Application {
|
||||
@@ -38,10 +43,17 @@ interface Application {
|
||||
|
||||
const NODE_WIDTH = 220;
|
||||
const NODE_HEIGHT = 60;
|
||||
const HORIZONTAL_SPACING = 280;
|
||||
const APP_NODE_WIDTH = 160;
|
||||
const APP_NODE_HEIGHT = 40;
|
||||
const HORIZONTAL_SPACING = 700;
|
||||
const VERTICAL_SPACING = 80;
|
||||
const START_Y = 120;
|
||||
const ROOT_NODE_WIDTH = 300;
|
||||
const CONTAINER_PADDING = 40;
|
||||
const COLUMN_SPACING = 220;
|
||||
const VM_APP_SPACING = 220;
|
||||
const MIN_VM_SPACING = 10;
|
||||
const APP_ROW_SPACING = 15;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -54,11 +66,181 @@ export async function GET() {
|
||||
}) as Promise<Application[]>,
|
||||
]);
|
||||
|
||||
// Level 2: Physical Servers
|
||||
const serverNodes: Node[] = servers
|
||||
.filter(server => !server.hostServer)
|
||||
.map((server, index, filteredServers) => {
|
||||
const xPos =
|
||||
index * HORIZONTAL_SPACING -
|
||||
((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2;
|
||||
|
||||
return {
|
||||
id: `server-${server.id}`,
|
||||
type: "server",
|
||||
data: {
|
||||
label: `${server.name}\n${server.ip}`,
|
||||
...server,
|
||||
},
|
||||
position: { x: xPos, y: START_Y },
|
||||
style: {
|
||||
background: "#ffffff",
|
||||
color: "#0f0f0f",
|
||||
border: "2px solid #e6e4e1",
|
||||
borderRadius: "4px",
|
||||
padding: "8px",
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT,
|
||||
fontSize: "0.9rem",
|
||||
lineHeight: "1.2",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Level 3: Services and VMs
|
||||
const serviceNodes: Node[] = [];
|
||||
const vmNodes: Node[] = [];
|
||||
|
||||
servers.forEach((server) => {
|
||||
const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`);
|
||||
if (serverNode) {
|
||||
const serverX = serverNode.position.x;
|
||||
|
||||
// Services (left column)
|
||||
applications
|
||||
.filter(app => app.serverId === server.id)
|
||||
.forEach((app, appIndex) => {
|
||||
serviceNodes.push({
|
||||
id: `service-${app.id}`,
|
||||
type: "service",
|
||||
data: {
|
||||
label: `${app.name}\n${app.localURL}`,
|
||||
...app,
|
||||
},
|
||||
position: {
|
||||
x: serverX - COLUMN_SPACING,
|
||||
y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20),
|
||||
},
|
||||
style: {
|
||||
background: "#f0f9ff",
|
||||
color: "#0f0f0f",
|
||||
border: "2px solid #60a5fa",
|
||||
borderRadius: "4px",
|
||||
padding: "6px",
|
||||
width: APP_NODE_WIDTH,
|
||||
height: APP_NODE_HEIGHT,
|
||||
fontSize: "0.8rem",
|
||||
lineHeight: "1.1",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// VMs (middle column) mit dynamischem Abstand
|
||||
const hostVMs = servers.filter(vm => vm.hostServer === server.id);
|
||||
let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING;
|
||||
|
||||
hostVMs.forEach(vm => {
|
||||
const appCount = applications.filter(app => app.serverId === vm.id).length;
|
||||
|
||||
vmNodes.push({
|
||||
id: `vm-${vm.id}`,
|
||||
type: "vm",
|
||||
data: {
|
||||
label: `${vm.name}\n${vm.ip}`,
|
||||
...vm,
|
||||
},
|
||||
position: {
|
||||
x: serverX,
|
||||
y: currentY,
|
||||
},
|
||||
style: {
|
||||
background: "#fef2f2",
|
||||
color: "#0f0f0f",
|
||||
border: "2px solid #fecaca",
|
||||
borderRadius: "4px",
|
||||
padding: "6px",
|
||||
width: APP_NODE_WIDTH,
|
||||
height: APP_NODE_HEIGHT,
|
||||
fontSize: "0.8rem",
|
||||
lineHeight: "1.1",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
});
|
||||
|
||||
// Dynamischer Abstand basierend auf Anzahl Apps
|
||||
const requiredSpace = appCount > 0
|
||||
? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING))
|
||||
: 0;
|
||||
|
||||
currentY += Math.max(
|
||||
requiredSpace + MIN_VM_SPACING,
|
||||
MIN_VM_SPACING + APP_NODE_HEIGHT
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Level 4: VM Applications (right column)
|
||||
const vmAppNodes: Node[] = [];
|
||||
vmNodes.forEach((vm) => {
|
||||
const vmX = vm.position.x;
|
||||
applications
|
||||
.filter(app => app.serverId === vm.data.id)
|
||||
.forEach((app, appIndex) => {
|
||||
vmAppNodes.push({
|
||||
id: `vm-app-${app.id}`,
|
||||
type: "application",
|
||||
data: {
|
||||
label: `${app.name}\n${app.localURL}`,
|
||||
...app,
|
||||
},
|
||||
position: {
|
||||
x: vmX + VM_APP_SPACING,
|
||||
y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20),
|
||||
},
|
||||
style: {
|
||||
background: "#f5f5f5",
|
||||
color: "#0f0f0f",
|
||||
border: "2px solid #e6e4e1",
|
||||
borderRadius: "4px",
|
||||
padding: "6px",
|
||||
width: APP_NODE_WIDTH,
|
||||
height: APP_NODE_HEIGHT,
|
||||
fontSize: "0.8rem",
|
||||
lineHeight: "1.1",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate dimensions for root node positioning
|
||||
const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes];
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
tempNodes.forEach((node) => {
|
||||
const width = parseInt(node.style.width?.toString() || "0", 10);
|
||||
const height = parseInt(node.style.height?.toString() || "0", 10);
|
||||
|
||||
minX = Math.min(minX, node.position.x);
|
||||
maxX = Math.max(maxX, node.position.x + width);
|
||||
minY = Math.min(minY, node.position.y);
|
||||
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: 0, y: 20 },
|
||||
position: { x: rootX, y: 0 },
|
||||
style: {
|
||||
background: "#ffffff",
|
||||
color: "#0f0f0f",
|
||||
@@ -72,95 +254,147 @@ export async function GET() {
|
||||
},
|
||||
};
|
||||
|
||||
const serverNodes: Node[] = servers.map((server, index) => {
|
||||
const xPos =
|
||||
index * HORIZONTAL_SPACING -
|
||||
((servers.length - 1) * HORIZONTAL_SPACING) / 2;
|
||||
// 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);
|
||||
|
||||
return {
|
||||
id: `server-${server.id}`,
|
||||
type: "server",
|
||||
data: {
|
||||
label: `${server.name}\n${server.ip}`,
|
||||
...server,
|
||||
},
|
||||
position: { x: xPos, y: START_Y },
|
||||
style: {
|
||||
background: "#ffffff",
|
||||
color: "#0f0f0f",
|
||||
border: "2px solid #e6e4e1",
|
||||
borderRadius: "4px",
|
||||
padding: "8px",
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT,
|
||||
fontSize: "0.9rem",
|
||||
lineHeight: "1.2",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
};
|
||||
});
|
||||
// Container Node
|
||||
const containerNode: Node = {
|
||||
id: 'container',
|
||||
type: 'container',
|
||||
data: { label: '' },
|
||||
position: {
|
||||
x: newMinX - CONTAINER_PADDING,
|
||||
y: newMinY - CONTAINER_PADDING
|
||||
},
|
||||
style: {
|
||||
width: newMaxX - newMinX + 2 * CONTAINER_PADDING,
|
||||
height: newMaxY - newMinY + 2 * CONTAINER_PADDING,
|
||||
background: 'transparent',
|
||||
border: '2px dashed #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
zIndex: 0,
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
zIndex: -1,
|
||||
};
|
||||
|
||||
const appNodes: Node[] = [];
|
||||
servers.forEach((server) => {
|
||||
const serverX =
|
||||
serverNodes.find((n) => n.id === `server-${server.id}`)?.position.x || 0;
|
||||
const serverY = START_Y;
|
||||
// Connections with hierarchical chaining
|
||||
const connections: Edge[] = [];
|
||||
|
||||
applications
|
||||
.filter((app) => app.serverId === server.id)
|
||||
.forEach((app, appIndex) => {
|
||||
appNodes.push({
|
||||
id: `app-${app.id}`,
|
||||
type: "application",
|
||||
data: {
|
||||
label: `${app.name}\n${app.localURL}`,
|
||||
...app,
|
||||
},
|
||||
position: {
|
||||
x: serverX,
|
||||
y: serverY + NODE_HEIGHT + 40 + appIndex * VERTICAL_SPACING,
|
||||
},
|
||||
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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const connections: Edge[] = [
|
||||
...servers.map((server) => ({
|
||||
// Root to Servers
|
||||
serverNodes.forEach((server) => {
|
||||
connections.push({
|
||||
id: `conn-root-${server.id}`,
|
||||
source: "root",
|
||||
target: `server-${server.id}`,
|
||||
target: 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,
|
||||
},
|
||||
})),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
// Services chaining
|
||||
const servicesByServer = new Map<number, Node[]>();
|
||||
serviceNodes.forEach(service => {
|
||||
const serverId = service.data.serverId;
|
||||
if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []);
|
||||
servicesByServer.get(serverId)!.push(service);
|
||||
});
|
||||
servicesByServer.forEach((services, serverId) => {
|
||||
services.sort((a, b) => a.position.y - b.position.y);
|
||||
services.forEach((service, index) => {
|
||||
if (index === 0) {
|
||||
connections.push({
|
||||
id: `conn-service-${service.id}`,
|
||||
source: `server-${serverId}`,
|
||||
target: service.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#60a5fa", strokeWidth: 2 },
|
||||
});
|
||||
} else {
|
||||
const prevService = services[index - 1];
|
||||
connections.push({
|
||||
id: `conn-service-${service.id}-${prevService.id}`,
|
||||
source: prevService.id,
|
||||
target: service.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#60a5fa", strokeWidth: 2 },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// VMs chaining
|
||||
const vmsByHost = new Map<number, Node[]>();
|
||||
vmNodes.forEach(vm => {
|
||||
const hostId = vm.data.hostServer;
|
||||
if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []);
|
||||
vmsByHost.get(hostId)!.push(vm);
|
||||
});
|
||||
vmsByHost.forEach((vms, hostId) => {
|
||||
vms.sort((a, b) => a.position.y - b.position.y);
|
||||
vms.forEach((vm, index) => {
|
||||
if (index === 0) {
|
||||
connections.push({
|
||||
id: `conn-vm-${vm.id}`,
|
||||
source: `server-${hostId}`,
|
||||
target: vm.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||
});
|
||||
} else {
|
||||
const prevVm = vms[index - 1];
|
||||
connections.push({
|
||||
id: `conn-vm-${vm.id}-${prevVm.id}`,
|
||||
source: prevVm.id,
|
||||
target: vm.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// VM Applications chaining
|
||||
const appsByVM = new Map<number, Node[]>();
|
||||
vmAppNodes.forEach(app => {
|
||||
const vmId = app.data.serverId;
|
||||
if (!appsByVM.has(vmId)) appsByVM.set(vmId, []);
|
||||
appsByVM.get(vmId)!.push(app);
|
||||
});
|
||||
appsByVM.forEach((apps, vmId) => {
|
||||
apps.sort((a, b) => a.position.y - b.position.y);
|
||||
apps.forEach((app, index) => {
|
||||
if (index === 0) {
|
||||
connections.push({
|
||||
id: `conn-vm-app-${app.id}`,
|
||||
source: `vm-${vmId}`,
|
||||
target: app.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||
});
|
||||
} else {
|
||||
const prevApp = apps[index - 1];
|
||||
connections.push({
|
||||
id: `conn-vm-app-${app.id}-${prevApp.id}`,
|
||||
source: prevApp.id,
|
||||
target: app.id,
|
||||
type: "straight",
|
||||
style: { stroke: "#f87171", strokeWidth: 2 },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
nodes: [rootNode, ...serverNodes, ...appNodes],
|
||||
nodes: [containerNode, ...allNodes],
|
||||
edges: connections,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
|
||||
59
app/api/notifications/add/route.ts
Normal file
59
app/api/notifications/add/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface AddRequest {
|
||||
type: string;
|
||||
name: 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, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser } = body;
|
||||
|
||||
const notification = await prisma.notification.create({
|
||||
data: {
|
||||
type: type,
|
||||
name: name,
|
||||
smtpHost: smtpHost,
|
||||
smtpPort: smtpPort,
|
||||
smtpFrom: smtpFrom,
|
||||
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
21
app/api/notifications/delete/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const id = Number(body.id);
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.notification.delete({
|
||||
where: { id: id }
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
16
app/api/notifications/get/route.ts
Normal file
16
app/api/notifications/get/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
||||
const notifications = await prisma.notification.findMany();
|
||||
|
||||
return NextResponse.json({
|
||||
notifications
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
23
app/api/notifications/test/route.ts
Normal file
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";
|
||||
|
||||
interface AddRequest {
|
||||
host: boolean;
|
||||
hostServer: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
os: string;
|
||||
ip: string;
|
||||
url: string;
|
||||
@@ -10,24 +13,31 @@ interface AddRequest {
|
||||
gpu: string;
|
||||
ram: string;
|
||||
disk: string;
|
||||
monitoring: boolean;
|
||||
monitoringURL: string;
|
||||
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: AddRequest = await request.json();
|
||||
const { name, os, ip, url, cpu, gpu, ram, disk } = body;
|
||||
const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
|
||||
|
||||
const server = await prisma.server.create({
|
||||
data: {
|
||||
host,
|
||||
hostServer,
|
||||
name,
|
||||
icon,
|
||||
os,
|
||||
ip,
|
||||
url,
|
||||
cpu,
|
||||
gpu,
|
||||
ram,
|
||||
disk
|
||||
disk,
|
||||
monitoring,
|
||||
monitoringURL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,21 @@ export async function POST(request: NextRequest) {
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Missing ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if there are any applications associated with the server
|
||||
const applications = await prisma.application.findMany({
|
||||
where: { serverId: id }
|
||||
});
|
||||
if (applications.length > 0) {
|
||||
return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Delete all server history records for this server
|
||||
await prisma.server_history.deleteMany({
|
||||
where: { serverId: id }
|
||||
});
|
||||
|
||||
// Delete the server
|
||||
await prisma.server.delete({
|
||||
where: { id: id }
|
||||
});
|
||||
|
||||
@@ -2,8 +2,11 @@ import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface EditRequest {
|
||||
host: boolean;
|
||||
hostServer: number;
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
os: string;
|
||||
ip: string;
|
||||
url: string;
|
||||
@@ -11,29 +14,43 @@ interface EditRequest {
|
||||
gpu: string;
|
||||
ram: string;
|
||||
disk: string;
|
||||
monitoring: boolean;
|
||||
monitoringURL: string;
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body: EditRequest = await request.json();
|
||||
const { id, name, os, ip, url, cpu, gpu, ram, disk } = body;
|
||||
const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body;
|
||||
|
||||
const existingServer = await prisma.server.findUnique({ where: { id } });
|
||||
if (!existingServer) {
|
||||
return NextResponse.json({ error: "Server not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
let newHostServer = hostServer;
|
||||
if (hostServer === null) {
|
||||
newHostServer = 0;
|
||||
} else {
|
||||
newHostServer = hostServer;
|
||||
}
|
||||
|
||||
const updatedServer = await prisma.server.update({
|
||||
where: { id },
|
||||
data: {
|
||||
host,
|
||||
hostServer: newHostServer,
|
||||
name,
|
||||
icon,
|
||||
os,
|
||||
ip,
|
||||
url,
|
||||
cpu,
|
||||
gpu,
|
||||
ram,
|
||||
disk
|
||||
disk,
|
||||
monitoring,
|
||||
monitoringURL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,250 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
interface GetRequest {
|
||||
page: number;
|
||||
page?: number;
|
||||
ITEMS_PER_PAGE?: number;
|
||||
timeRange?: '1h' | '1d' | '7d' | '30d';
|
||||
serverId?: number;
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 5;
|
||||
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) {
|
||||
try {
|
||||
const body: GetRequest = await request.json();
|
||||
const page = Math.max(1, body.page || 1);
|
||||
|
||||
const servers = await prisma.server.findMany({
|
||||
skip: (page - 1) * ITEMS_PER_PAGE,
|
||||
take: ITEMS_PER_PAGE,
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
|
||||
const timeRange = body.timeRange || '1h';
|
||||
const serverId = body.serverId;
|
||||
|
||||
const totalCount = await prisma.server.count();
|
||||
const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
|
||||
// If serverId is provided, only fetch that specific server
|
||||
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;
|
||||
let totalHosts = 0;
|
||||
if (!serverId) {
|
||||
totalHosts = await prisma.server.count({
|
||||
where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
|
||||
});
|
||||
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
servers,
|
||||
maxPage
|
||||
servers: hostsWithVms,
|
||||
maxPage,
|
||||
totalItems: totalHosts
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
21
app/api/servers/hosts/route.ts
Normal file
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 });
|
||||
}
|
||||
}
|
||||
38
app/api/servers/monitoring/route.ts
Normal file
38
app/api/servers/monitoring/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
uptime: true
|
||||
}
|
||||
});
|
||||
|
||||
const monitoringData = servers.map((server: {
|
||||
id: number;
|
||||
online: boolean;
|
||||
cpuUsage: string | null;
|
||||
ramUsage: string | null;
|
||||
diskUsage: string | null;
|
||||
uptime: string | null;
|
||||
}) => ({
|
||||
id: server.id,
|
||||
online: server.online,
|
||||
cpuUsage: server.cpuUsage ? parseInt(server.cpuUsage) : 0,
|
||||
ramUsage: server.ramUsage ? parseInt(server.ramUsage) : 0,
|
||||
diskUsage: server.diskUsage ? parseInt(server.diskUsage) : 0,
|
||||
uptime: server.uptime || ""
|
||||
}));
|
||||
|
||||
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 { searchterm } = body;
|
||||
|
||||
// Fetch all servers
|
||||
const servers = await prisma.server.findMany({});
|
||||
|
||||
// Create a map of host servers with their hosted VMs
|
||||
const serverMap = new Map();
|
||||
servers.forEach(server => {
|
||||
if (server.host) {
|
||||
serverMap.set(server.id, {
|
||||
...server,
|
||||
isVM: false,
|
||||
hostedVMs: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add VMs to their host servers and mark them as VMs
|
||||
const serversWithType = servers.map(server => {
|
||||
// If not a host and has a hostServer, it's a VM
|
||||
if (!server.host && server.hostServer) {
|
||||
const hostServer = serverMap.get(server.hostServer);
|
||||
if (hostServer) {
|
||||
hostServer.hostedVMs.push({
|
||||
...server,
|
||||
isVM: true
|
||||
});
|
||||
}
|
||||
return {
|
||||
...server,
|
||||
isVM: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
...server,
|
||||
isVM: false,
|
||||
hostedVMs: serverMap.get(server.id)?.hostedVMs || []
|
||||
};
|
||||
});
|
||||
|
||||
const fuseOptions = {
|
||||
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'],
|
||||
keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
};
|
||||
|
||||
const fuse = new Fuse(servers, fuseOptions);
|
||||
const fuse = new Fuse(serversWithType, fuseOptions);
|
||||
|
||||
const searchResults = fuse.search(searchterm);
|
||||
|
||||
|
||||
25
app/api/settings/get_notification_text/route.ts
Normal file
25
app/api/settings/get_notification_text/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check if there are any settings entries
|
||||
const existingSettings = await prisma.settings.findFirst();
|
||||
if (!existingSettings) {
|
||||
return NextResponse.json({ "notification_text_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
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,64 @@
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import axios from "axios"
|
||||
import Link from "next/link"
|
||||
import { Activity, Layers, Network, Server } from "lucide-react"
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios"; // Korrekter Import
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
interface StatsResponse {
|
||||
serverCount: number;
|
||||
applicationCount: number;
|
||||
onlineApplicationsCount: number;
|
||||
serverCountNoVMs: number
|
||||
serverCountOnlyVMs: number
|
||||
applicationCount: number
|
||||
onlineApplicationsCount: number
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const [serverCount, setServerCount] = useState<number>(0);
|
||||
const [applicationCount, setApplicationCount] = useState<number>(0);
|
||||
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0);
|
||||
const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
|
||||
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
|
||||
const [applicationCount, setApplicationCount] = useState<number>(0)
|
||||
const [onlineApplicationsCount, setOnlineApplicationsCount] = useState<number>(0)
|
||||
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const response = await axios.post<StatsResponse>('/api/dashboard/get', {});
|
||||
setServerCount(response.data.serverCount);
|
||||
setApplicationCount(response.data.applicationCount);
|
||||
setOnlineApplicationsCount(response.data.onlineApplicationsCount);
|
||||
const response = await axios.post<StatsResponse>("/api/dashboard/get", {})
|
||||
setServerCountNoVMs(response.data.serverCountNoVMs)
|
||||
setServerCountOnlyVMs(response.data.serverCountOnlyVMs)
|
||||
setApplicationCount(response.data.applicationCount)
|
||||
setOnlineApplicationsCount(response.data.onlineApplicationsCount)
|
||||
} catch (error: any) {
|
||||
console.log("Axios error:", error.response?.data);
|
||||
console.log("Axios error:", error.response?.data)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getStats();
|
||||
}, []);
|
||||
getStats()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<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>
|
||||
<BreadcrumbPage>/</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
@@ -66,51 +68,162 @@ export default function Dashboard() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">{serverCount}</span>
|
||||
<span className="text-md">Servers</span>
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="overflow-hidden border-t-4 border-t-rose-500 shadow-lg transition-all hover:shadow-xl hover:border-t-rose-600">
|
||||
<CardHeader className="py-3 pb-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-semibold">Servers</CardTitle>
|
||||
<CardDescription className="mt-1">Physical and virtual servers overview</CardDescription>
|
||||
</div>
|
||||
<Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">{applicationCount}</span>
|
||||
<span className="text-md">Applications</span>
|
||||
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Physical Servers */}
|
||||
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
|
||||
<div className="bg-rose-100 p-2 rounded-full">
|
||||
<Server className="h-6 w-6 text-rose-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold">{serverCountNoVMs}</div>
|
||||
<p className="text-sm text-muted-foreground">Physical Servers</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Virtual Machines */}
|
||||
<div className="flex items-center space-x-4 border border-gray-background p-4 rounded-lg">
|
||||
<div className="bg-violet-100 p-2 rounded-full">
|
||||
<Network className="h-6 w-6 text-violet-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold">{serverCountOnlyVMs}</div>
|
||||
<p className="text-sm text-muted-foreground">Virtual Servers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/servers" className="flex items-center justify-between">
|
||||
<span>Manage Servers</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-amber-500 shadow-lg transition-all hover:shadow-xl hover:border-t-amber-600">
|
||||
<CardHeader className="py-3 pb-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-semibold">Applications</CardTitle>
|
||||
<CardDescription className="mt-1">Manage your deployed applications</CardDescription>
|
||||
</div>
|
||||
<Layers className="h-8 w-8 text-amber-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||
<div className="text-4xl font-bold">{applicationCount}</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Running applications</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/applications" className="flex items-center justify-between">
|
||||
<span>View all applications</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-emerald-500 shadow-lg transition-all hover:shadow-xl hover:border-t-emerald-600">
|
||||
<CardHeader className="py-3 pb-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-semibold">Uptime</CardTitle>
|
||||
<CardDescription className="mt-1">Monitor your service availability</CardDescription>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-4xl font-bold flex items-center justify-between">
|
||||
<span>
|
||||
{onlineApplicationsCount}/{applicationCount}
|
||||
</span>
|
||||
<span className="text-md">Applications are online</span>
|
||||
<div className="flex items-center bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-lg font-semibold">
|
||||
{applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-3">
|
||||
<div
|
||||
className="bg-emerald-500 h-2.5 rounded-full"
|
||||
style={{
|
||||
width: `${applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/uptime" className="flex items-center justify-between">
|
||||
<span>View uptime metrics</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-t-4 border-t-sky-500 shadow-lg transition-all hover:shadow-xl hover:border-t-sky-600">
|
||||
<CardHeader className="py-3 pb-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-semibold">Network</CardTitle>
|
||||
<CardDescription className="mt-1">Manage network configuration</CardDescription>
|
||||
</div>
|
||||
<Network className="h-8 w-8 text-sky-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-1 pb-2 min-h-[120px]">
|
||||
<div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Active connections</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full font-semibold transition-colors border border-muted-foreground/20 hover:bg-primary hover:text-primary-foreground"
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/network" className="flex items-center justify-between">
|
||||
<span>View network details</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
|
||||
<span className="text-gray-400 text-2xl">COMING SOON</span>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="h-72 w-full rounded-xl flex items-center justify-center bg-muted">
|
||||
<span className="text-gray-400 text-2xl">COMING SOON</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
List,
|
||||
Pencil,
|
||||
Zap,
|
||||
ViewIcon,
|
||||
Grid3X3,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
@@ -65,8 +68,23 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { StatusIndicator } from "@/components/status-indicator";
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Application {
|
||||
id: number;
|
||||
@@ -78,6 +96,7 @@ interface Application {
|
||||
server?: string;
|
||||
online: boolean;
|
||||
serverId: number;
|
||||
uptimecheckUrl?: string;
|
||||
}
|
||||
|
||||
interface Server {
|
||||
@@ -89,6 +108,7 @@ interface ApplicationsResponse {
|
||||
applications: Application[];
|
||||
servers: Server[];
|
||||
maxPage: number;
|
||||
totalItems?: number;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
@@ -98,6 +118,8 @@ export default function Dashboard() {
|
||||
const [publicURL, setPublicURL] = useState<string>("");
|
||||
const [localURL, setLocalURL] = useState<string>("");
|
||||
const [serverId, setServerId] = useState<number | null>(null);
|
||||
const [customUptimeCheck, setCustomUptimeCheck] = useState<boolean>(false);
|
||||
const [uptimecheckUrl, setUptimecheckUrl] = useState<string>("");
|
||||
|
||||
const [editName, setEditName] = useState<string>("");
|
||||
const [editDescription, setEditDescription] = useState<string>("");
|
||||
@@ -106,34 +128,85 @@ export default function Dashboard() {
|
||||
const [editLocalURL, setEditLocalURL] = useState<string>("");
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [editServerId, setEditServerId] = useState<number | null>(null);
|
||||
const [editCustomUptimeCheck, setEditCustomUptimeCheck] = useState<boolean>(false);
|
||||
const [editUptimecheckUrl, setEditUptimecheckUrl] = useState<string>("");
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [maxPage, setMaxPage] = useState<number>(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [isSearching, setIsSearching] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const savedLayout = Cookies.get("layoutPreference-app");
|
||||
const layout_bool = savedLayout === "grid";
|
||||
setIsGridLayout(layout_bool);
|
||||
setItemsPerPage(layout_bool ? 15 : 5);
|
||||
}, []);
|
||||
const savedLayout = Cookies.get("layoutPreference-app");
|
||||
const savedItemsPerPage = Cookies.get("itemsPerPage-app");
|
||||
const initialIsGridLayout = savedLayout === "grid";
|
||||
const initialIsCompactLayout = savedLayout === "compact";
|
||||
const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5);
|
||||
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
|
||||
|
||||
const toggleLayout = () => {
|
||||
const newLayout = !isGridLayout;
|
||||
setIsGridLayout(newLayout);
|
||||
Cookies.set("layoutPreference-app", newLayout ? "grid" : "standard", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
setItemsPerPage(newLayout ? 15 : 5);
|
||||
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
|
||||
const [isCompactLayout, setIsCompactLayout] = useState<boolean>(initialIsCompactLayout);
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const toggleLayout = (layout: string) => {
|
||||
if (layout === "standard") {
|
||||
setIsGridLayout(false);
|
||||
setIsCompactLayout(false);
|
||||
Cookies.set("layoutPreference-app", "standard", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (layout === "grid") {
|
||||
setIsGridLayout(true);
|
||||
setIsCompactLayout(false);
|
||||
Cookies.set("layoutPreference-app", "grid", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (layout === "compact") {
|
||||
setIsGridLayout(false);
|
||||
setIsCompactLayout(true);
|
||||
Cookies.set("layoutPreference-app", "compact", {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (value: string) => {
|
||||
// Clear any existing timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timer
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Ensure the value is within the valid range
|
||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||
toast.error("Please enter a number between 1 and 100");
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1); // Reset to first page when changing items per page
|
||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}, 300); // 300ms delay
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
@@ -145,10 +218,13 @@ export default function Dashboard() {
|
||||
publicURL,
|
||||
localURL,
|
||||
serverId,
|
||||
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
|
||||
});
|
||||
getApplications();
|
||||
toast.success("Application added successfully");
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to add application");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,12 +238,21 @@ export default function Dashboard() {
|
||||
setApplications(response.data.applications);
|
||||
setServers(response.data.servers);
|
||||
setMaxPage(response.data.maxPage);
|
||||
if (response.data.totalItems !== undefined) {
|
||||
setTotalItems(response.data.totalItems);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to get applications");
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate current range of items being displayed
|
||||
const [totalItems, setTotalItems] = useState<number>(0);
|
||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
useEffect(() => {
|
||||
getApplications();
|
||||
}, [currentPage, itemsPerPage]);
|
||||
@@ -180,8 +265,10 @@ export default function Dashboard() {
|
||||
try {
|
||||
await axios.post("/api/applications/delete", { id });
|
||||
getApplications();
|
||||
toast.success("Application deleted successfully");
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to delete application");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,6 +280,14 @@ export default function Dashboard() {
|
||||
setEditIcon(app.icon || "");
|
||||
setEditLocalURL(app.localURL || "");
|
||||
setEditPublicURL(app.publicURL || "");
|
||||
|
||||
if (app.uptimecheckUrl) {
|
||||
setEditCustomUptimeCheck(true);
|
||||
setEditUptimecheckUrl(app.uptimecheckUrl);
|
||||
} else {
|
||||
setEditCustomUptimeCheck(false);
|
||||
setEditUptimecheckUrl("");
|
||||
}
|
||||
};
|
||||
|
||||
const edit = async () => {
|
||||
@@ -207,11 +302,14 @@ export default function Dashboard() {
|
||||
icon: editIcon,
|
||||
publicURL: editPublicURL,
|
||||
localURL: editLocalURL,
|
||||
uptimecheckUrl: editCustomUptimeCheck ? editUptimecheckUrl : "",
|
||||
});
|
||||
getApplications();
|
||||
setEditId(null);
|
||||
toast.success("Application edited successfully");
|
||||
} catch (error: any) {
|
||||
console.log(error.response.data);
|
||||
toast.error("Failed to edit application");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -254,7 +352,7 @@ export default function Dashboard() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<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" />
|
||||
@@ -275,24 +373,120 @@ export default function Dashboard() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-2xl font-semibold">Your Applications</span>
|
||||
<span className="text-3xl font-bold">Your Applications</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleLayout}
|
||||
title={
|
||||
isGridLayout ? "Switch to list view" : "Switch to grid view"
|
||||
}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" title="Change view">
|
||||
{isCompactLayout ? (
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
) : isGridLayout ? (
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
) : (
|
||||
<List className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
|
||||
<List className="h-4 w-4 mr-2" /> List View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
|
||||
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
|
||||
<Grid3X3 className="h-4 w-4 mr-2" /> Compact View
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
onValueChange={handleItemsPerPageChange}
|
||||
onOpenChange={(open) => {
|
||||
if (open && customInputRef.current) {
|
||||
customInputRef.current.value = String(itemsPerPage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGridLayout ? (
|
||||
<List className="h-4 w-4" />
|
||||
) : (
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
<SelectItem value="5">5 items</SelectItem>
|
||||
<SelectItem value="10">10 items</SelectItem>
|
||||
<SelectItem value="15">15 items</SelectItem>
|
||||
<SelectItem value="20">20 items</SelectItem>
|
||||
<SelectItem value="25">25 items</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
ref={customInputRef}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
className="h-8"
|
||||
defaultValue={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
// Don't immediately apply the change while typing
|
||||
// Just validate the input for visual feedback
|
||||
const value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1 || value > 100) {
|
||||
e.target.classList.add("border-red-500");
|
||||
} else {
|
||||
e.target.classList.remove("border-red-500");
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// Apply the change when the input loses focus
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
handleItemsPerPageChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Clear any existing debounce timer to apply immediately
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
const value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
// Apply change immediately on Enter
|
||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1);
|
||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Close the dropdown
|
||||
document.body.click();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{servers.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
You must first add a server.
|
||||
@@ -358,9 +552,18 @@ export default function Dashboard() {
|
||||
placeholder="https://example.com/icon.png"
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Generate Icon URL
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
@@ -380,6 +583,36 @@ export default function Dashboard() {
|
||||
onChange={(e) => setLocalURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="custom-uptime-check"
|
||||
checked={customUptimeCheck}
|
||||
onChange={(e) => setCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{customUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={uptimecheckUrl}
|
||||
onChange={(e) => setUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -409,86 +642,307 @@ export default function Dashboard() {
|
||||
{!loading ? (
|
||||
<div
|
||||
className={
|
||||
isGridLayout
|
||||
isCompactLayout
|
||||
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2"
|
||||
: isGridLayout
|
||||
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{applications.map((app) => (
|
||||
<Card
|
||||
key={app.id}
|
||||
className={
|
||||
isGridLayout
|
||||
? "h-full flex flex-col justify-between relative"
|
||||
: "w-full mb-4 relative"
|
||||
}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="absolute top-2 right-2">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full flex items-center justify-center ${
|
||||
app.online ? "bg-green-700" : "bg-red-700"
|
||||
}`}
|
||||
title={app.online ? "Online" : "Offline"}
|
||||
>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
app.online ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
isCompactLayout ? (
|
||||
<div
|
||||
key={app.id}
|
||||
className="bg-card rounded-md border p-3 flex flex-col items-center justify-between h-[120px] w-full cursor-pointer hover:shadow-md transition-shadow relative"
|
||||
onClick={() => window.open(app.publicURL, "_blank")}
|
||||
title={app.name}
|
||||
>
|
||||
<div className="absolute top-1 right-1">
|
||||
<StatusIndicator isOnline={app.online} showLabel={false} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
|
||||
{app.icon ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Image</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
{app.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-md">
|
||||
{app.description}
|
||||
<br />
|
||||
Server: {app.server || "No server"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
|
||||
{app.icon ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Icon</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<h3 className="text-sm font-medium truncate w-full max-w-[110px]">{app.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
key={app.id}
|
||||
className={
|
||||
isGridLayout
|
||||
? "h-full flex flex-col justify-between relative"
|
||||
: "w-full mb-4 relative"
|
||||
}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="absolute top-2 right-2">
|
||||
<StatusIndicator isOnline={app.online} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end justify-start space-y-2 w-[270px]">
|
||||
<div className={`flex ${isGridLayout ? 'flex-col' : 'items-center justify-between'} w-full mt-4 mb-4`}>
|
||||
<div className="flex items-center">
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
|
||||
{app.icon ? (
|
||||
<img
|
||||
src={app.icon}
|
||||
alt={app.name}
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Image</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
{app.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-md">
|
||||
{app.description}
|
||||
{app.description && (
|
||||
<br className="hidden md:block" />
|
||||
)}
|
||||
Server: {app.server || "No server"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isGridLayout && (
|
||||
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex flex-col space-y-2 flex-grow">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.publicURL, "_blank")
|
||||
}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Public URL
|
||||
</Button>
|
||||
{app.localURL && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.localURL, "_blank")
|
||||
}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Local URL
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={() => deleteApplication(app.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={() => openEditDialog(app)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Edit Application
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Name</Label>
|
||||
<Input
|
||||
placeholder="e.g. Portainer"
|
||||
value={editName}
|
||||
onChange={(e) =>
|
||||
setEditName(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Server</Label>
|
||||
<Select
|
||||
value={
|
||||
editServerId !== null
|
||||
? String(editServerId)
|
||||
: undefined
|
||||
}
|
||||
onValueChange={(v) =>
|
||||
setEditServerId(Number(v))
|
||||
}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{servers.map((server) => (
|
||||
<SelectItem
|
||||
key={server.id}
|
||||
value={String(server.id)}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Description{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Application description"
|
||||
value={editDescription}
|
||||
onChange={(e) =>
|
||||
setEditDescription(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Icon URL{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="https://example.com/icon.png"
|
||||
value={editIcon}
|
||||
onChange={(e) =>
|
||||
setEditIcon(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={generateEditIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Public URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={editPublicURL}
|
||||
onChange={(e) =>
|
||||
setEditPublicURL(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Local URL{" "}
|
||||
<span className="text-stone-600">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:3000"
|
||||
value={editLocalURL}
|
||||
onChange={(e) =>
|
||||
setEditLocalURL(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-custom-uptime-check"
|
||||
checked={editCustomUptimeCheck}
|
||||
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{editCustomUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={editUptimecheckUrl}
|
||||
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={edit}
|
||||
disabled={
|
||||
!editName || !editPublicURL || !editServerId
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{isGridLayout && (
|
||||
<CardFooter className="mt-auto">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex flex-col space-y-2 flex-grow">
|
||||
<div className={`grid ${app.localURL ? 'grid-cols-2' : 'grid-cols-1'} gap-2 flex-grow`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.publicURL, "_blank")
|
||||
}
|
||||
onClick={() => window.open(app.publicURL, "_blank")}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Open Public URL
|
||||
Public URL
|
||||
</Button>
|
||||
{app.localURL && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 w-full"
|
||||
onClick={() =>
|
||||
window.open(app.localURL, "_blank")
|
||||
}
|
||||
onClick={() => window.open(app.localURL, "_blank")}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Open Local URL
|
||||
Local URL
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
@@ -612,6 +1066,36 @@ export default function Dashboard() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-custom-uptime-check"
|
||||
checked={editCustomUptimeCheck}
|
||||
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{editCustomUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
value={editUptimecheckUrl}
|
||||
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -630,10 +1114,10 @@ export default function Dashboard() {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -664,14 +1148,19 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4">
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={handlePrevious}
|
||||
isActive={currentPage > 1}
|
||||
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
@@ -679,9 +1168,9 @@ export default function Dashboard() {
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={handleNext}
|
||||
isActive={currentPage < maxPage}
|
||||
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function Dashboard() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="flex flex-col h-screen">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<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 dark:text-white" />
|
||||
<Separator
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
748
app/dashboard/servers/[server_id]/Server.tsx
Normal file
748
app/dashboard/servers/[server_id]/Server.tsx
Normal file
@@ -0,0 +1,748 @@
|
||||
"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;
|
||||
uptime?: string;
|
||||
}
|
||||
|
||||
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 flex flex-col items-end">
|
||||
<StatusIndicator isOnline={server.online} />
|
||||
{server.online && server.uptime && (
|
||||
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
|
||||
since {server.uptime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<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 && (
|
||||
<div className="flex flex-col items-end">
|
||||
<StatusIndicator isOnline={hostedVM.online} />
|
||||
{hostedVM.online && hostedVM.uptime && (
|
||||
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
|
||||
since {hostedVM.uptime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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
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,35 +1,279 @@
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
"use client"
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useEffect, useState } from "react"
|
||||
import axios from "axios"
|
||||
import Cookies from "js-cookie"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
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 {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
const [email, setEmail] = useState<string>("")
|
||||
const [password, setPassword] = useState<string>("")
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("")
|
||||
const [oldPassword, setOldPassword] = useState<string>("")
|
||||
|
||||
const [emailError, setEmailError] = useState<string>("")
|
||||
const [passwordError, setPasswordError] = useState<string>("")
|
||||
const [emailErrorVisible, setEmailErrorVisible] = useState<boolean>(false)
|
||||
const [passwordErrorVisible, setPasswordErrorVisible] = useState<boolean>(false)
|
||||
|
||||
const [passwordSuccess, setPasswordSuccess] = useState<boolean>(false)
|
||||
const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
|
||||
|
||||
const [notificationType, setNotificationType] = useState<string>("")
|
||||
const [notificationName, setNotificationName] = useState<string>("")
|
||||
const [smtpHost, setSmtpHost] = useState<string>("")
|
||||
const [smtpPort, setSmtpPort] = useState<number>(0)
|
||||
const [smtpSecure, setSmtpSecure] = useState<boolean>(false)
|
||||
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 () => {
|
||||
setEmailErrorVisible(false)
|
||||
setEmailSuccess(false)
|
||||
setEmailError("")
|
||||
|
||||
if (!email) {
|
||||
setEmailError("Email is required")
|
||||
setEmailErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setEmailErrorVisible(false)
|
||||
setEmailError("")
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await axios.post("/api/auth/edit_email", {
|
||||
newEmail: email,
|
||||
jwtToken: Cookies.get("token"),
|
||||
})
|
||||
setEmailSuccess(true)
|
||||
setEmail("")
|
||||
setTimeout(() => {
|
||||
setEmailSuccess(false)
|
||||
}, 3000)
|
||||
} catch (error: any) {
|
||||
setEmailError(error.response.data.error)
|
||||
setEmailErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setEmailErrorVisible(false)
|
||||
setEmailError("")
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
try {
|
||||
if (password !== confirmPassword) {
|
||||
setPasswordError("Passwords do not match")
|
||||
setPasswordErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setPasswordErrorVisible(false)
|
||||
setPasswordError("")
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
if (!oldPassword || !password || !confirmPassword) {
|
||||
setPasswordError("All fields are required")
|
||||
setPasswordErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setPasswordErrorVisible(false)
|
||||
setPasswordError("")
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/auth/edit_password", {
|
||||
oldPassword: oldPassword,
|
||||
newPassword: password,
|
||||
jwtToken: Cookies.get("token"),
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
setPasswordSuccess(true)
|
||||
setPassword("")
|
||||
setOldPassword("")
|
||||
setConfirmPassword("")
|
||||
setTimeout(() => {
|
||||
setPasswordSuccess(false)
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error: any) {
|
||||
setPasswordErrorVisible(true)
|
||||
setPasswordError(error.response.data.error)
|
||||
setTimeout(() => {
|
||||
setPasswordErrorVisible(false)
|
||||
setPasswordError("")
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const addNotification = async () => {
|
||||
try {
|
||||
const response = await axios.post("/api/notifications/add", {
|
||||
name: notificationName,
|
||||
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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<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" />
|
||||
@@ -50,32 +294,540 @@ export default function Settings() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="pl-4 pr-4">
|
||||
<span className="text-2xl font-semibold">Settings</span>
|
||||
<div className="pt-4">
|
||||
<Card className="w-full mb-4 relative">
|
||||
<CardHeader>
|
||||
<span className="text-xl font-bold">Theme</span>
|
||||
<Select
|
||||
value={theme}
|
||||
onValueChange={(value: string) => setTheme(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full [&_svg]:hidden">
|
||||
<SelectValue>
|
||||
{(theme ?? 'system').charAt(0).toUpperCase() + (theme ?? 'system').slice(1)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="p-6">
|
||||
<div className="pb-4">
|
||||
<span className="text-3xl font-bold">Settings</span>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-xl font-semibold">User Settings</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
Manage your user settings here. You can change your email, password, and other account settings.
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="font-semibold text-lg">Change Email</h3>
|
||||
</div>
|
||||
|
||||
{emailErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{emailError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{emailSuccess && (
|
||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||
<Check className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Email changed successfully.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter new email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changeEmail} className="w-full h-11">
|
||||
Change Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="font-semibold text-lg">Change Password</h3>
|
||||
</div>
|
||||
|
||||
{passwordErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{passwordError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{passwordSuccess && (
|
||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||
<Check className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Password changed successfully.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter old password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changePassword} className="w-full h-11">
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-xl font-semibold">Theme Settings</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
Select a theme for the application. You can choose between light, dark, or system theme.
|
||||
</div>
|
||||
|
||||
<div className="max-w-md">
|
||||
<Select value={theme} onValueChange={(value: string) => setTheme(value)}>
|
||||
<SelectTrigger className="w-full h-11">
|
||||
<SelectValue>
|
||||
{(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||
<div className="flex items-center gap-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>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
id="notificationName"
|
||||
placeholder="Notification Name (optional)"
|
||||
onChange={(e) => setNotificationName(e.target.value)}
|
||||
/>
|
||||
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Notification Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="smtp">SMTP</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
<SelectItem value="discord">Discord</SelectItem>
|
||||
<SelectItem value="gotify">Gotify</SelectItem>
|
||||
<SelectItem value="ntfy">Ntfy</SelectItem>
|
||||
<SelectItem value="pushover">Pushover</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
{notificationType === "smtp" && (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="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>
|
||||
</div>
|
||||
</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.name && notification.name !== "" ? notification.name : notification.type}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{notification.type === "smtp" && "Email notifications"}
|
||||
{notification.type === "telegram" && "Telegram bot alerts"}
|
||||
{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>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
439
app/dashboard/uptime/Uptime.tsx
Normal file
439
app/dashboard/uptime/Uptime.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import axios from "axios";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationLink,
|
||||
} from "@/components/ui/pagination";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const timeFormats = {
|
||||
1: (timestamp: string) =>
|
||||
new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}),
|
||||
2: (timestamp: string) =>
|
||||
new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}),
|
||||
3: (timestamp: string) =>
|
||||
new Date(timestamp).toLocaleDateString([], {
|
||||
day: '2-digit',
|
||||
month: 'short'
|
||||
}),
|
||||
4: (timestamp: string) =>
|
||||
new Date(timestamp).toLocaleDateString([], {
|
||||
day: '2-digit',
|
||||
month: 'short'
|
||||
})
|
||||
};
|
||||
|
||||
const minBoxWidths = {
|
||||
1: 20,
|
||||
2: 20,
|
||||
3: 24,
|
||||
4: 24
|
||||
};
|
||||
|
||||
interface UptimeData {
|
||||
appName: string;
|
||||
appId: number;
|
||||
uptimeSummary: {
|
||||
timestamp: string;
|
||||
missing: boolean;
|
||||
online: boolean | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface PaginationData {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
export default function Uptime() {
|
||||
const [data, setData] = useState<UptimeData[]>([]);
|
||||
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
|
||||
const [pagination, setPagination] = useState<PaginationData>({
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const savedItemsPerPage = Cookies.get("itemsPerPage-uptime");
|
||||
const defaultItemsPerPage = 5;
|
||||
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
|
||||
|
||||
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await axios.post<{
|
||||
data: UptimeData[];
|
||||
pagination: PaginationData;
|
||||
}>("/api/applications/uptime", {
|
||||
timespan: selectedTimespan,
|
||||
page,
|
||||
itemsPerPage
|
||||
});
|
||||
|
||||
setData(response.data.data);
|
||||
setPagination(response.data.pagination);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
setData([]);
|
||||
setPagination({
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 0
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
const newPage = Math.max(1, pagination.currentPage - 1);
|
||||
setPagination(prev => ({...prev, currentPage: newPage}));
|
||||
getData(timespan, newPage, itemsPerPage);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
|
||||
setPagination(prev => ({...prev, currentPage: newPage}));
|
||||
getData(timespan, newPage, itemsPerPage);
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (value: string) => {
|
||||
// Clear any existing timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timer
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
const newItemsPerPage = parseInt(value);
|
||||
|
||||
// Ensure the value is within the valid range
|
||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||
toast.error("Please enter a number between 1 and 100");
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setPagination(prev => ({...prev, currentPage: 1})); // Reset to first page
|
||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Fetch data with new pagination
|
||||
getData(timespan, 1, validatedValue);
|
||||
}, 300); // 300ms delay
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getData(timespan, 1, itemsPerPage);
|
||||
}, [timespan]);
|
||||
|
||||
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>
|
||||
<BreadcrumbPage>Uptime</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Uptime</span>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
onValueChange={handleItemsPerPageChange}
|
||||
onOpenChange={(open) => {
|
||||
if (open && customInputRef.current) {
|
||||
customInputRef.current.value = String(itemsPerPage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
<SelectItem value="5">5 items</SelectItem>
|
||||
<SelectItem value="10">10 items</SelectItem>
|
||||
<SelectItem value="15">15 items</SelectItem>
|
||||
<SelectItem value="20">20 items</SelectItem>
|
||||
<SelectItem value="25">25 items</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
ref={customInputRef}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
className="h-8"
|
||||
defaultValue={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
// Don't immediately apply the change while typing
|
||||
// Just validate the input for visual feedback
|
||||
const value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1 || value > 100) {
|
||||
e.target.classList.add("border-red-500");
|
||||
} else {
|
||||
e.target.classList.remove("border-red-500");
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// Apply the change when the input loses focus
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
handleItemsPerPageChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Clear any existing debounce timer to apply immediately
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
const value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
// Apply change immediately on Enter
|
||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||
setItemsPerPage(validatedValue);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
getData(timespan, 1, validatedValue);
|
||||
|
||||
// Close the dropdown
|
||||
document.body.click();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={String(timespan)}
|
||||
onValueChange={(v) => {
|
||||
setTimespan(Number(v) as 1 | 2 | 3 | 4);
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
||||
<SelectItem value="2">Last 1 day</SelectItem>
|
||||
<SelectItem value="3">Last 7 days</SelectItem>
|
||||
<SelectItem value="4">Last 30 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : (
|
||||
data.map((app) => {
|
||||
const reversedSummary = [...app.uptimeSummary].reverse();
|
||||
const startTime = reversedSummary[0]?.timestamp;
|
||||
const endTime = reversedSummary[reversedSummary.length - 1]?.timestamp;
|
||||
|
||||
return (
|
||||
<Card key={app.appId}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">{app.appName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>{startTime ? timeFormats[timespan](startTime) : ""}</span>
|
||||
<span>{endTime ? timeFormats[timespan](endTime) : ""}</span>
|
||||
</div>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<div
|
||||
className="grid gap-0.5 w-full pb-2"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${minBoxWidths[timespan]}px, 1fr))`
|
||||
}}
|
||||
>
|
||||
{reversedSummary.map((entry) => (
|
||||
<Tooltip.Root key={entry.timestamp}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div
|
||||
className={`h-8 w-full rounded-sm border transition-colors ${
|
||||
entry.missing
|
||||
? "bg-gray-300 border-gray-400"
|
||||
: entry.online
|
||||
? "bg-green-500 border-green-600"
|
||||
: "bg-red-500 border-red-600"
|
||||
}`}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="rounded bg-gray-900 px-2 py-1 text-white text-xs shadow-lg"
|
||||
side="top"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium">
|
||||
{new Date(entry.timestamp).toLocaleString([], {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: timespan > 2 ? 'numeric' : undefined,
|
||||
hour: '2-digit',
|
||||
minute: timespan === 1 ? '2-digit' : undefined,
|
||||
hour12: false
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{entry.missing
|
||||
? "No data"
|
||||
: entry.online
|
||||
? "Online"
|
||||
: "Offline"}
|
||||
</p>
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-gray-900" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pagination.totalItems > 0 && !isLoading && (
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{pagination.totalItems > 0
|
||||
? `Showing ${((pagination.currentPage - 1) * itemsPerPage) + 1}-${Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems)} of ${pagination.totalItems} items`
|
||||
: "No items found"}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={handlePrevious}
|
||||
aria-disabled={pagination.currentPage === 1 || isLoading}
|
||||
className={
|
||||
pagination.currentPage === 1 || isLoading
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>{pagination.currentPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={handleNext}
|
||||
aria-disabled={pagination.currentPage === pagination.totalPages || isLoading}
|
||||
className={
|
||||
pagination.currentPage === pagination.totalPages || isLoading
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
59
app/dashboard/uptime/page.tsx
Normal file
59
app/dashboard/uptime/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 Uptime from "./Uptime";
|
||||
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 clip-path='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' stroke-width='1.4' stroke-linecap='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 ? <Uptime /> : null;
|
||||
}
|
||||
206
app/page.tsx
206
app/page.tsx
@@ -1,108 +1,148 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Cookies from "js-cookie"
|
||||
import { useRouter } from "next/navigation"
|
||||
import axios from "axios"
|
||||
import { AlertCircle, KeyRound, Mail, User } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert"
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { useRouter } from "next/navigation";
|
||||
import axios from "axios";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
|
||||
export default function Home() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState('');
|
||||
const [errorVisible, setErrorVisible] = useState(false);
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [rememberMe, setRememberMe] = useState(false)
|
||||
const router = useRouter()
|
||||
const [error, setError] = useState("")
|
||||
const [errorVisible, setErrorVisible] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const token = Cookies.get('token');
|
||||
const token = Cookies.get("token")
|
||||
if (token) {
|
||||
router.push('/dashboard');
|
||||
router.push("/dashboard")
|
||||
}
|
||||
}, [router]);
|
||||
}, [router])
|
||||
|
||||
interface LoginResponse {
|
||||
token: string;
|
||||
token: string
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', { username, password });
|
||||
const { token } = response.data as LoginResponse;
|
||||
Cookies.set('token', token);
|
||||
router.push('/dashboard');
|
||||
} catch (error: any) {
|
||||
setError(error.response.data.error);
|
||||
if (!username || !password) {
|
||||
setError("Please enter both email and password")
|
||||
setErrorVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await axios.post("/api/auth/login", { username, password })
|
||||
const { token } = response.data as LoginResponse
|
||||
|
||||
const cookieOptions = rememberMe ? { expires: 7 } : {}
|
||||
Cookies.set("token", token, cookieOptions)
|
||||
|
||||
router.push("/dashboard")
|
||||
} catch (error: any) {
|
||||
setError(error.response?.data?.error || "Login failed. Please try again.")
|
||||
setErrorVisible(true)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen items-center justify-center gap-6 ">
|
||||
<Card className="w-1/3">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your Login data of the compose.yml file below to access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{errorVisible && (
|
||||
<>
|
||||
<div className="pb-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="mail@example.com"
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input id="password" type="password" required placeholder="* * * * * * *" onChange={(e) => setPassword(e.target.value)}/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={login}>
|
||||
Login
|
||||
</Button>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/30 p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<KeyRound className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-foreground">CoreControl</h1>
|
||||
<p className="text-muted-foreground">Sign in to access your dashboard</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-muted/40 shadow-lg">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-semibold">Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{errorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50 slide-in-from-top-5">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Authentication Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="mail@example.com"
|
||||
className="pl-10"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-sm font-medium">
|
||||
Password
|
||||
</Label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="pl-10"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button className="w-full" onClick={login} disabled={isLoading}>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import * as React from "react"
|
||||
"use client"
|
||||
|
||||
import type * as React from "react"
|
||||
import Image from "next/image"
|
||||
import { AppWindow, Settings, LayoutDashboardIcon, Briefcase, Server, Network } from "lucide-react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
AppWindow,
|
||||
Settings,
|
||||
LayoutDashboardIcon,
|
||||
Briefcase,
|
||||
Server,
|
||||
Network,
|
||||
Activity,
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
@@ -13,12 +28,15 @@ import {
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
SidebarFooter,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
import Cookies from "js-cookie"
|
||||
import { useRouter } from "next/navigation"
|
||||
import packageJson from "@/package.json"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
|
||||
interface NavItem {
|
||||
title: string
|
||||
@@ -33,7 +51,7 @@ const data: { navMain: NavItem[] } = {
|
||||
{
|
||||
title: "Dashboard",
|
||||
icon: LayoutDashboardIcon,
|
||||
url: "/dashboard"
|
||||
url: "/dashboard",
|
||||
},
|
||||
{
|
||||
title: "My Infrastructure",
|
||||
@@ -50,11 +68,16 @@ const data: { navMain: NavItem[] } = {
|
||||
icon: AppWindow,
|
||||
url: "/dashboard/applications",
|
||||
},
|
||||
{
|
||||
title: "Uptime",
|
||||
icon: Activity,
|
||||
url: "/dashboard/uptime",
|
||||
},
|
||||
{
|
||||
title: "Network",
|
||||
icon: Network,
|
||||
url: "/dashboard/network",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -67,71 +90,119 @@ const data: { navMain: NavItem[] } = {
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const router = useRouter()
|
||||
|
||||
const pathname = usePathname()
|
||||
|
||||
const logout = async () => {
|
||||
Cookies.remove('token')
|
||||
Cookies.remove("token")
|
||||
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 (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarHeader className="border-b border-sidebar-border/30 pb-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="https://github.com/crocofied/corecontrol">
|
||||
<Image src="/logo.png" width={48} height={48} alt="Logo"/>
|
||||
<SidebarMenuButton size="lg" asChild className="gap-3">
|
||||
<a href="https://github.com/crocofied/corecontrol" className="transition-all hover:opacity-80">
|
||||
<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">
|
||||
<span className="font-semibold">CoreControl</span>
|
||||
<span className="">v{packageJson.version}</span>
|
||||
<span className="font-semibold text-base">CoreControl</span>
|
||||
<span className="text-xs text-sidebar-foreground/70">v{packageJson.version}</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent className="flex flex-col h-full">
|
||||
|
||||
<SidebarContent className="flex flex-col h-full py-4">
|
||||
<SidebarGroup className="flex-grow">
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url} className="font-medium">
|
||||
{item.icon && <item.icon className="mr-2" />}
|
||||
{item.title}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
{item.items?.length && (
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={subItem.isActive ?? false}
|
||||
<SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2">
|
||||
Main Navigation
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) =>
|
||||
item.items?.length ? (
|
||||
<Collapsible key={item.title} defaultOpen={hasActiveChild(item.items)} className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"font-medium transition-all",
|
||||
(hasActiveChild(item.items) || isActive(item.url)) &&
|
||||
"text-sidebar-accent-foreground bg-sidebar-accent/50",
|
||||
)}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
{subItem.icon && <subItem.icon className="mr-2" />}
|
||||
{subItem.title}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild isActive={isActive(subItem.url)} className="transition-all">
|
||||
<Link href={subItem.url} className="flex items-center">
|
||||
{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>
|
||||
|
||||
<div className="p-4">
|
||||
<Button variant="destructive" className="w-full" onClick={logout}>
|
||||
<SidebarFooter className="border-t border-sidebar-border/30 pt-4 mt-auto">
|
||||
<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
|
||||
</Button>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
38
components/status-indicator.tsx
Normal file
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>
|
||||
)
|
||||
}
|
||||
66
components/ui/accordion.tsx
Normal file
66
components/ui/accordion.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
46
components/ui/badge.tsx
Normal file
46
components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
32
components/ui/checkbox.tsx
Normal file
32
components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
33
components/ui/collapsible.tsx
Normal file
33
components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
31
components/ui/progress.tsx
Normal file
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
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
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 }
|
||||
16
compose.yml
16
compose.yml
@@ -4,19 +4,16 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
LOGIN_EMAIL: "mail@example.com"
|
||||
LOGIN_PASSWORD: "SecretPassword"
|
||||
JWT_SECRET: RANDOM_SECRET
|
||||
ACCOUNT_SECRET: RANDOM_SECRET
|
||||
JWT_SECRET: RANDOM_SECRET # Replace with a secure random string
|
||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||
depends_on:
|
||||
- db
|
||||
- agent
|
||||
|
||||
agent:
|
||||
image: haedlessdev/corecontrol-agent:latest
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:17
|
||||
@@ -27,6 +24,11 @@ services:
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
66
docs/.vitepress/config.mts
Normal file
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
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.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
<script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
|
||||
<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.DiWcjlN4.js
vendored
Normal file
1
docs/.vitepress/dist/assets/app.DiWcjlN4.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{t as p}from"./chunks/theme.9-rJywIy.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.DPDPlp3K.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp};
|
||||
BIN
docs/.vitepress/dist/assets/applications_add_button.CTBM75AA.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/applications_add_button.CTBM75AA.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/.vitepress/dist/assets/applications_display.D1ZCmp63.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/applications_display.D1ZCmp63.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BgQKcov9.js
vendored
Normal file
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BgQKcov9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.CnOOacsu.js
vendored
Normal file
8
docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.CnOOacsu.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
docs/.vitepress/dist/assets/chunks/framework.DPDPlp3K.js
vendored
Normal file
18
docs/.vitepress/dist/assets/chunks/framework.DPDPlp3K.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/.vitepress/dist/assets/chunks/metadata.d21683cf.js
vendored
Normal file
1
docs/.vitepress/dist/assets/chunks/metadata.d21683cf.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\":\"BeIP42w_\",\"installation.md\":\"Cz1eOHOr\",\"notifications_discord.md\":\"C0x5CxmR\",\"notifications_email.md\":\"Cugw2BRs\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"vFHjr6ko\",\"notifications_ntfy.md\":\"CPMnGQVP\",\"notifications_pushover.md\":\"lZwGAQ0A\",\"notifications_telegram.md\":\"B6_EzaEX\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"CoreControl\",\"description\":\"Dashboard to manage your entire server infrastructure\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"logo\":\"/logo.png\",\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Installation\",\"link\":\"/installation\"}],\"footer\":{\"message\":\"Released under the MIT License.\",\"copyright\":\"Copyright © 2025-present CoreControl\"},\"search\":{\"provider\":\"local\"},\"sidebar\":[{\"text\":\"Deploy\",\"items\":[{\"text\":\"Installation\",\"link\":\"/installation\"}]},{\"text\":\"General\",\"items\":[{\"text\":\"Dashboard\",\"link\":\"/general/Dashboard\"},{\"text\":\"Servers\",\"link\":\"/general/Servers\"},{\"text\":\"Applications\",\"link\":\"/general/Applications\"},{\"text\":\"Uptime\",\"link\":\"/general/Uptime\"},{\"text\":\"Network\",\"link\":\"/general/Network\"},{\"text\":\"Settings\",\"link\":\"/general/Settings\"}]},{\"text\":\"Notifications\",\"items\":[{\"text\":\"General\",\"link\":\"/notifications/General\"},{\"text\":\"Email\",\"link\":\"/notifications/Email\"},{\"text\":\"Telegram\",\"link\":\"/notifications/Telegram\"},{\"text\":\"Discord\",\"link\":\"/notifications/Discord\"},{\"text\":\"Gotify\",\"link\":\"/notifications/Gotify\"},{\"text\":\"Ntfy\",\"link\":\"/notifications/Ntfy\"},{\"text\":\"Pushover\",\"link\":\"/notifications/Pushover\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");
|
||||
1
docs/.vitepress/dist/assets/chunks/settings_notifications.DL7eQG4d.js
vendored
Normal file
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.9-rJywIy.js
vendored
Normal file
2
docs/.vitepress/dist/assets/chunks/theme.9-rJywIy.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/.vitepress/dist/assets/dashboard_card_applications.DErIEBeJ.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/dashboard_card_applications.DErIEBeJ.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_network.C2YTU2ur.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/dashboard_card_network.C2YTU2ur.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_servers.DNNhRbkY.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/dashboard_card_servers.DNNhRbkY.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/.vitepress/dist/assets/dashboard_card_uptime.B6cdZ6Ju.png
vendored
Normal file
BIN
docs/.vitepress/dist/assets/dashboard_card_uptime.B6cdZ6Ju.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
1
docs/.vitepress/dist/assets/general_Applications.md.DFVqSlCw.js
vendored
Normal file
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
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
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
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
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
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
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
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
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
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
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
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.BeIP42w_.js
vendored
Normal file
1
docs/.vitepress/dist/assets/index.md.BeIP42w_.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 with alerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};
|
||||
1
docs/.vitepress/dist/assets/index.md.BeIP42w_.lean.js
vendored
Normal file
1
docs/.vitepress/dist/assets/index.md.BeIP42w_.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 with alerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};
|
||||
36
docs/.vitepress/dist/assets/installation.md.Cz1eOHOr.js
vendored
Normal file
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
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-ext.r48I6akx.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2
vendored
Normal file
BIN
docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user