v1.0.0->main
v1.0.0
42
Dockerfile
@ -1,47 +1,51 @@
|
||||
# Builder Stage
|
||||
FROM node:20-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
||||
|
||||
ARG TARGETARCH # Wird automatisch von Buildx gesetzt
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN case ${TARGETARCH} in \
|
||||
"amd64") export PRISMA_CLI_BINARY_TARGETS="linux-musl-x64-openssl-3.0.x" ;; \
|
||||
"arm64") export PRISMA_CLI_BINARY_TARGETS="linux-musl-arm64-openssl-3.0.x" ;; \
|
||||
"arm") export PRISMA_CLI_BINARY_TARGETS="linux-musl-arm-openssl-3.0.x" ;; \
|
||||
*) echo "Unsupported ARCH: ${TARGETARCH}" && exit 1 ;; \
|
||||
esac
|
||||
|
||||
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
|
||||
FROM --platform=$TARGETPLATFORM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV PRISMA_CLI_BINARY_TARGETS="linux-musl-arm64-openssl-3.0.x"
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# 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
|
||||
RUN npm prune --production
|
||||
|
||||
# Run migrations and start
|
||||
EXPOSE 3000
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
||||
|
||||
|
||||
# - - BUILD COMMAND - -
|
||||
# docker buildx build \
|
||||
# --platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
# -t haedlessdev/corecontrol:1.0.0 \
|
||||
# -t haedlessdev/corecontrol:latest \
|
||||
# --push \
|
||||
# .
|
||||
@ -1,25 +1,45 @@
|
||||
# --- Build Stage ---
|
||||
FROM golang:1.19-alpine AS builder
|
||||
# Multi-Arch Builder mit expliziter Plattform-Angabe
|
||||
FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS builder
|
||||
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=$TARGETOS \
|
||||
GOARCH=$TARGETARCH
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app ./cmd/agent
|
||||
# Cross-Compile für Zielarchitektur
|
||||
RUN go build -ldflags="-w -s" -o app ./cmd/agent
|
||||
|
||||
# --- Run Stage ---
|
||||
# Multi-Arch Laufzeit-Image
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
# Notwendig für TLS/SSL-Zertifikate
|
||||
RUN apk --no-cache add ca-certificates gcompat
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/app .
|
||||
|
||||
# Security Hardening
|
||||
USER nobody:nobody
|
||||
ENV GOMAXPROCS=1
|
||||
|
||||
CMD ["./app"]
|
||||
|
||||
# - - BUILD COMMAND - -
|
||||
# docker buildx build \
|
||||
# --platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
# -t haedlessdev/corecontrol-agent:1.0.0 \
|
||||
# -t haedlessdev/corecontrol-agent:latest \
|
||||
# --push \
|
||||
# .
|
||||
@ -86,7 +86,7 @@ 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"
|
||||
"pushoverUrl", "pushoverToken", "pushoverUser", "echobellURL"
|
||||
FROM notification
|
||||
WHERE enabled = true`,
|
||||
)
|
||||
@ -103,7 +103,7 @@ func LoadNotifications(db *sql.DB) ([]models.Notification, error) {
|
||||
&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,
|
||||
&n.PushoverUrl, &n.PushoverToken, &n.PushoverUser, &n.EchobellURL,
|
||||
); err != nil {
|
||||
fmt.Printf("Error scanning notification: %v\n", err)
|
||||
continue
|
||||
|
||||
@ -21,6 +21,8 @@ type Server struct {
|
||||
CpuUsage sql.NullFloat64
|
||||
RamUsage sql.NullFloat64
|
||||
DiskUsage sql.NullFloat64
|
||||
GpuUsage sql.NullFloat64
|
||||
Temp sql.NullFloat64
|
||||
Uptime sql.NullString
|
||||
}
|
||||
|
||||
@ -51,6 +53,26 @@ type UptimeResponse struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type GPUResponse struct {
|
||||
Proc float64 `json:"proc"`
|
||||
}
|
||||
|
||||
type TemperatureResponse struct {
|
||||
Composite []struct {
|
||||
Label string `json:"label"`
|
||||
Unit string `json:"unit"`
|
||||
Value float64 `json:"value"`
|
||||
Warning float64 `json:"warning"`
|
||||
Critical float64 `json:"critical"`
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
} `json:"Composite"`
|
||||
}
|
||||
|
||||
type TempResponse struct {
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int
|
||||
Enabled bool
|
||||
@ -72,4 +94,5 @@ type Notification struct {
|
||||
PushoverUrl sql.NullString
|
||||
PushoverToken sql.NullString
|
||||
PushoverUser sql.NullString
|
||||
EchobellURL sql.NullString
|
||||
}
|
||||
|
||||
@ -84,6 +84,10 @@ func (ns *NotificationSender) SendSpecificNotification(n models.Notification, me
|
||||
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
|
||||
ns.sendPushover(n, message)
|
||||
}
|
||||
case "echobell":
|
||||
if n.EchobellURL.Valid {
|
||||
ns.sendEchobell(n, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,3 +241,26 @@ func (ns *NotificationSender) sendPushover(n models.Notification, message string
|
||||
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *NotificationSender) sendEchobell(n models.Notification, message string) {
|
||||
jsonData := fmt.Sprintf(`{"message": "%s"}`, message)
|
||||
req, err := http.NewRequest("POST", n.EchobellURL.String, strings.NewReader(jsonData))
|
||||
if err != nil {
|
||||
fmt.Printf("Echobell: ERROR creating request: %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("Echobell: ERROR sending request: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Echobell: ERROR status code: %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,12 +8,21 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/corecontrol/agent/internal/models"
|
||||
"github.com/corecontrol/agent/internal/notifications"
|
||||
)
|
||||
|
||||
// notificationState tracks the last known status for each server
|
||||
var notificationState = struct {
|
||||
sync.RWMutex
|
||||
lastStatus map[int]bool
|
||||
}{
|
||||
lastStatus: make(map[int]bool),
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -31,16 +40,18 @@ func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, no
|
||||
fmt.Printf("%s Checking...\n", logPrefix)
|
||||
|
||||
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
|
||||
var cpuUsage, ramUsage, diskUsage float64
|
||||
var cpuUsage, ramUsage, diskUsage, gpuUsage, temp 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)
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, 0, 0, "")
|
||||
if shouldSendNotification(server.ID, online) {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -51,9 +62,11 @@ func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, no
|
||||
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)
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, 0, 0, "")
|
||||
if shouldSendNotification(server.ID, online) {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
ramUsage = memUsage
|
||||
@ -62,29 +75,55 @@ func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, no
|
||||
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)
|
||||
updateServerStatus(db, server.ID, false, 0, 0, 0, 0, 0, "")
|
||||
if shouldSendNotification(server.ID, online) {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
addServerHistoryEntry(db, server.ID, false, 0, 0, 0, 0, 0)
|
||||
continue
|
||||
}
|
||||
diskUsage = diskUsageVal
|
||||
|
||||
// Get GPU usage
|
||||
_, gpuUsageVal := fetchGPUUsage(client, baseURL, logPrefix)
|
||||
gpuUsage = gpuUsageVal
|
||||
|
||||
// Get Temperature
|
||||
_, tempVal := fetchTemperature(client, baseURL, logPrefix)
|
||||
temp = tempVal
|
||||
|
||||
// Check if status changed and send notification if needed
|
||||
if online != server.Online {
|
||||
if online != server.Online && shouldSendNotification(server.ID, online) {
|
||||
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
|
||||
}
|
||||
|
||||
// Update server status with metrics
|
||||
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, gpuUsage, temp, uptimeStr)
|
||||
|
||||
// Add entry to server history
|
||||
addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
|
||||
addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage, gpuUsage, temp)
|
||||
|
||||
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n",
|
||||
logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr)
|
||||
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, GPU: %.2f%%, Temp: %.2f°C, Uptime: %s\n",
|
||||
logPrefix, cpuUsage, ramUsage, diskUsage, gpuUsage, temp, uptimeStr)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldSendNotification checks if a notification should be sent based on status change
|
||||
func shouldSendNotification(serverID int, online bool) bool {
|
||||
notificationState.Lock()
|
||||
defer notificationState.Unlock()
|
||||
|
||||
lastStatus, exists := notificationState.lastStatus[serverID]
|
||||
|
||||
// If this is the first check or status has changed
|
||||
if !exists || lastStatus != online {
|
||||
notificationState.lastStatus[serverID] = online
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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))
|
||||
@ -194,6 +233,56 @@ func fetchUptime(client *http.Client, baseURL, logPrefix string) string {
|
||||
return uptimeStr
|
||||
}
|
||||
|
||||
// Helper function to fetch GPU usage
|
||||
func fetchGPUUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
gpuResp, err := client.Get(fmt.Sprintf("%s/api/4/gpu", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s GPU request failed: %v\n", logPrefix, err)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
defer gpuResp.Body.Close()
|
||||
|
||||
if gpuResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad GPU status code: %d\n", logPrefix, gpuResp.StatusCode)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
|
||||
var gpuData models.GPUResponse
|
||||
if err := json.NewDecoder(gpuResp.Body).Decode(&gpuData); err != nil {
|
||||
fmt.Printf("%s Failed to parse GPU JSON: %v\n", logPrefix, err)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
|
||||
return true, gpuData.Proc
|
||||
}
|
||||
|
||||
// Helper function to fetch temperature
|
||||
func fetchTemperature(client *http.Client, baseURL, logPrefix string) (bool, float64) {
|
||||
tempResp, err := client.Get(fmt.Sprintf("%s/api/4/sensors/label/value/Composite", baseURL))
|
||||
if err != nil {
|
||||
fmt.Printf("%s Temperature request failed: %v\n", logPrefix, err)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
defer tempResp.Body.Close()
|
||||
|
||||
if tempResp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("%s Bad temperature status code: %d\n", logPrefix, tempResp.StatusCode)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
|
||||
var tempData models.TemperatureResponse
|
||||
if err := json.NewDecoder(tempResp.Body).Decode(&tempData); err != nil {
|
||||
fmt.Printf("%s Failed to parse temperature JSON: %v\n", logPrefix, err)
|
||||
return true, 0 // Return true to indicate server is still online
|
||||
}
|
||||
|
||||
if len(tempData.Composite) > 0 {
|
||||
return true, tempData.Composite[0].Value
|
||||
}
|
||||
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// Helper function to send notification about status change
|
||||
func sendStatusChangeNotification(server models.Server, online bool, template string, notifSender *notifications.NotificationSender) {
|
||||
status := "offline"
|
||||
@ -208,14 +297,14 @@ func sendStatusChangeNotification(server models.Server, online bool, template st
|
||||
}
|
||||
|
||||
// Helper function to update server status
|
||||
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) {
|
||||
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage, gpuUsage, temp 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,
|
||||
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "gpuUsage" = $5::float8, "temp" = $6::float8, "uptime" = $7
|
||||
WHERE id = $8`,
|
||||
online, cpuUsage, ramUsage, diskUsage, gpuUsage, temp, uptime, serverID,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
|
||||
@ -223,15 +312,16 @@ func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsag
|
||||
}
|
||||
|
||||
// Helper function to add server history entry
|
||||
func addServerHistoryEntry(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) {
|
||||
func addServerHistoryEntry(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage, gpuUsage, temp 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),
|
||||
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "gpuUsage", "temp", "createdAt"
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())`,
|
||||
serverID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage),
|
||||
fmt.Sprintf("%.2f", diskUsage), fmt.Sprintf("%.2f", gpuUsage), fmt.Sprintf("%.2f", temp),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to insert server history (ID: %d): %v\n", serverID, err)
|
||||
|
||||
@ -21,12 +21,13 @@ interface AddRequest {
|
||||
pushoverUrl?: string;
|
||||
pushoverToken?: string;
|
||||
pushoverUser?: string;
|
||||
echobellURL?: 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 { type, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser, echobellURL } = body;
|
||||
|
||||
const notification = await prisma.notification.create({
|
||||
data: {
|
||||
@ -49,6 +50,7 @@ export async function POST(request: NextRequest) {
|
||||
pushoverUrl: pushoverUrl,
|
||||
pushoverToken: pushoverToken,
|
||||
pushoverUser: pushoverUser,
|
||||
echobellURL: echobellURL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -132,6 +132,8 @@ export async function POST(request: NextRequest) {
|
||||
cpu: number[],
|
||||
ram: number[],
|
||||
disk: number[],
|
||||
gpu: number[],
|
||||
temp: number[],
|
||||
online: boolean[]
|
||||
}>();
|
||||
|
||||
@ -142,6 +144,8 @@ export async function POST(request: NextRequest) {
|
||||
cpu: [],
|
||||
ram: [],
|
||||
disk: [],
|
||||
gpu: [],
|
||||
temp: [],
|
||||
online: []
|
||||
});
|
||||
});
|
||||
@ -167,6 +171,8 @@ export async function POST(request: NextRequest) {
|
||||
interval.cpu.push(parseUsageValue(record.cpuUsage));
|
||||
interval.ram.push(parseUsageValue(record.ramUsage));
|
||||
interval.disk.push(parseUsageValue(record.diskUsage));
|
||||
interval.gpu.push(parseUsageValue(record.gpuUsage));
|
||||
interval.temp.push(parseUsageValue(record.temp));
|
||||
interval.online.push(record.online);
|
||||
}
|
||||
});
|
||||
@ -178,6 +184,8 @@ export async function POST(request: NextRequest) {
|
||||
cpu: [],
|
||||
ram: [],
|
||||
disk: [],
|
||||
gpu: [],
|
||||
temp: [],
|
||||
online: []
|
||||
};
|
||||
|
||||
@ -189,6 +197,8 @@ export async function POST(request: NextRequest) {
|
||||
cpu: average(data.cpu),
|
||||
ram: average(data.ram),
|
||||
disk: average(data.disk),
|
||||
gpu: average(data.gpu),
|
||||
temp: average(data.temp),
|
||||
online: data.online.length ?
|
||||
data.online.filter(Boolean).length / data.online.length >= 0.5
|
||||
: null
|
||||
@ -221,6 +231,14 @@ export async function POST(request: NextRequest) {
|
||||
const data = historyMap.get(d.toISOString())?.disk || [];
|
||||
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
|
||||
}),
|
||||
gpu: intervals.map(d => {
|
||||
const data = historyMap.get(d.toISOString())?.gpu || [];
|
||||
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null;
|
||||
}),
|
||||
temp: intervals.map(d => {
|
||||
const data = historyMap.get(d.toISOString())?.temp || [];
|
||||
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;
|
||||
|
||||
@ -11,6 +11,8 @@ export async function GET() {
|
||||
cpuUsage: true,
|
||||
ramUsage: true,
|
||||
diskUsage: true,
|
||||
gpuUsage: true,
|
||||
temp: true,
|
||||
uptime: true
|
||||
}
|
||||
});
|
||||
@ -21,13 +23,17 @@ export async function GET() {
|
||||
cpuUsage: string | null;
|
||||
ramUsage: string | null;
|
||||
diskUsage: string | null;
|
||||
gpuUsage: string | null;
|
||||
temp: 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,
|
||||
cpuUsage: server.cpuUsage ? parseFloat(server.cpuUsage) : 0,
|
||||
ramUsage: server.ramUsage ? parseFloat(server.ramUsage) : 0,
|
||||
diskUsage: server.diskUsage ? parseFloat(server.diskUsage) : 0,
|
||||
gpuUsage: server.gpuUsage ? parseFloat(server.gpuUsage) : 0,
|
||||
temp: server.temp ? parseFloat(server.temp) : 0,
|
||||
uptime: server.uptime || ""
|
||||
}));
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ 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"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
interface StatsResponse {
|
||||
serverCountNoVMs: number
|
||||
@ -26,6 +27,7 @@ interface StatsResponse {
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations('Dashboard')
|
||||
const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
|
||||
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
|
||||
const [applicationCount, setApplicationCount] = useState<number>(0)
|
||||
@ -62,22 +64,22 @@ export default function Dashboard() {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Dashboard</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Title')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-6">Dashboard</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-6">{t('Title')}</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>
|
||||
<CardTitle className="text-2xl font-semibold">{t('Servers.Title')}</CardTitle>
|
||||
<CardDescription className="mt-1">{t('Servers.Description')}</CardDescription>
|
||||
</div>
|
||||
<Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
@ -91,7 +93,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold">{serverCountNoVMs}</div>
|
||||
<p className="text-sm text-muted-foreground">Physical Servers</p>
|
||||
<p className="text-sm text-muted-foreground">{t('Servers.PhysicalServers')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,7 +104,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold">{serverCountOnlyVMs}</div>
|
||||
<p className="text-sm text-muted-foreground">Virtual Servers</p>
|
||||
<p className="text-sm text-muted-foreground">{t('Servers.VirtualServers')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,7 +117,7 @@ export default function Dashboard() {
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/servers" className="flex items-center justify-between">
|
||||
<span>Manage Servers</span>
|
||||
<span>{t('Servers.ManageServers')}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@ -125,15 +127,15 @@ export default function Dashboard() {
|
||||
<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>
|
||||
<CardTitle className="text-2xl font-semibold">{t('Applications.Title')}</CardTitle>
|
||||
<CardDescription className="mt-1">{t('Applications.Description')}</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>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t('Applications.OnlineApplications')}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
@ -143,7 +145,7 @@ export default function Dashboard() {
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/applications" className="flex items-center justify-between">
|
||||
<span>View all applications</span>
|
||||
<span>{t('Applications.ViewAllApplications')}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@ -153,8 +155,8 @@ export default function Dashboard() {
|
||||
<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>
|
||||
<CardTitle className="text-2xl font-semibold">{t('Uptime.Title')}</CardTitle>
|
||||
<CardDescription className="mt-1">{t('Uptime.Description')}</CardDescription>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" />
|
||||
</div>
|
||||
@ -177,7 +179,7 @@ export default function Dashboard() {
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Online applications</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t('Uptime.OnlineApplications')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
@ -188,7 +190,7 @@ export default function Dashboard() {
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/uptime" className="flex items-center justify-between">
|
||||
<span>View uptime metrics</span>
|
||||
<span>{t('Uptime.ViewUptimeMetrics')}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@ -198,15 +200,15 @@ export default function Dashboard() {
|
||||
<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>
|
||||
<CardTitle className="text-2xl font-semibold">{t('Network.Title')}</CardTitle>
|
||||
<CardDescription className="mt-1">{t('Network.Description')}</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>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t('Network.ActiveConnections')}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t bg-muted/10 py-2 px-4">
|
||||
<Button
|
||||
@ -216,7 +218,7 @@ export default function Dashboard() {
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard/network" className="flex items-center justify-between">
|
||||
<span>View network details</span>
|
||||
<span>{t('Network.ViewNetworkDetails')}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@ -85,6 +85,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Application {
|
||||
id: number;
|
||||
@ -112,6 +113,7 @@ interface ApplicationsResponse {
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations();
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [icon, setIcon] = useState<string>("");
|
||||
@ -182,31 +184,28 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
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");
|
||||
toast.error(t('Applications.Messages.NumberValidation'));
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setCurrentPage(1); // Reset to first page when changing items per page
|
||||
setCurrentPage(1);
|
||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}, 300); // 300ms delay
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
@ -221,10 +220,10 @@ export default function Dashboard() {
|
||||
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
|
||||
});
|
||||
getApplications();
|
||||
toast.success("Application added successfully");
|
||||
toast.success(t('Applications.Messages.AddSuccess'));
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to add application");
|
||||
toast.error(t('Applications.Messages.AddError'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -244,7 +243,7 @@ export default function Dashboard() {
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to get applications");
|
||||
toast.error(t('Applications.Messages.GetError'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -265,10 +264,10 @@ export default function Dashboard() {
|
||||
try {
|
||||
await axios.post("/api/applications/delete", { id });
|
||||
getApplications();
|
||||
toast.success("Application deleted successfully");
|
||||
toast.success(t('Applications.Messages.DeleteSuccess'));
|
||||
} catch (error: any) {
|
||||
console.log(error.response?.data);
|
||||
toast.error("Failed to delete application");
|
||||
toast.error(t('Applications.Messages.DeleteError'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -306,10 +305,10 @@ export default function Dashboard() {
|
||||
});
|
||||
getApplications();
|
||||
setEditId(null);
|
||||
toast.success("Application edited successfully");
|
||||
toast.success(t('Applications.Messages.EditSuccess'));
|
||||
} catch (error: any) {
|
||||
console.log(error.response.data);
|
||||
toast.error("Failed to edit application");
|
||||
toast.error(t('Applications.Messages.EditError'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -363,11 +362,11 @@ export default function Dashboard() {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Applications.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Applications</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Applications.Breadcrumb.Applications')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
@ -376,11 +375,11 @@ export default function Dashboard() {
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Your Applications</span>
|
||||
<span className="text-3xl font-bold">{t('Applications.Title')}</span>
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" title="Change view">
|
||||
<Button variant="outline" size="icon" title={t('Applications.Views.ChangeView')}>
|
||||
{isCompactLayout ? (
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
) : isGridLayout ? (
|
||||
@ -392,13 +391,13 @@ export default function Dashboard() {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
|
||||
<List className="h-4 w-4 mr-2" /> List View
|
||||
<List className="h-4 w-4 mr-2" /> {t('Applications.Views.ListView')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
|
||||
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
|
||||
<LayoutGrid className="h-4 w-4 mr-2" /> {t('Applications.Views.GridView')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
|
||||
<Grid3X3 className="h-4 w-4 mr-2" /> Compact View
|
||||
<Grid3X3 className="h-4 w-4 mr-2" /> {t('Applications.Views.CompactView')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -489,7 +488,7 @@ export default function Dashboard() {
|
||||
</Select>
|
||||
{servers.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
You must first add a server.
|
||||
{t('Applications.Messages.AddServerFirst')}
|
||||
</p>
|
||||
) : (
|
||||
<AlertDialog>
|
||||
@ -498,26 +497,26 @@ export default function Dashboard() {
|
||||
<Plus />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Add an application</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('Applications.Add.Title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Name</Label>
|
||||
<Label>{t('Applications.Add.Name')}</Label>
|
||||
<Input
|
||||
placeholder="e.g. Portainer"
|
||||
placeholder={t('Applications.Add.NamePlaceholder')}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Server</Label>
|
||||
<Label>{t('Applications.Add.Server')}</Label>
|
||||
<Select
|
||||
onValueChange={(v) => setServerId(Number(v))}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server" />
|
||||
<SelectValue placeholder={t('Applications.Add.SelectServer')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{servers.map((server) => (
|
||||
@ -533,53 +532,41 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Description{" "}
|
||||
<span className="text-stone-600">(optional)</span>
|
||||
{t('Applications.Add.Description')}{" "}
|
||||
<span className="text-stone-600">{t('Common.optional')}</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Application description"
|
||||
placeholder={t('Applications.Add.DescriptionPlaceholder')}
|
||||
onChange={(e) => setDescription(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>
|
||||
<Label>{t('Applications.Add.IconURL')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={icon}
|
||||
placeholder="https://example.com/icon.png"
|
||||
placeholder={t('Applications.Add.IconURLPlaceholder')}
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Generate Icon URL
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
value={icon}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
||||
<Zap />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Public URL</Label>
|
||||
<Label>{t('Applications.Add.PublicURL')}</Label>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
placeholder={t('Applications.Add.PublicURLPlaceholder')}
|
||||
onChange={(e) => setPublicURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>
|
||||
Local URL{" "}
|
||||
<span className="text-stone-600">(optional)</span>
|
||||
{t('Applications.Add.LocalURL')}{" "}
|
||||
<span className="text-stone-600">{t('Common.optional')}</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:3000"
|
||||
placeholder={t('Applications.Add.LocalURLPlaceholder')}
|
||||
onChange={(e) => setLocalURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -591,23 +578,23 @@ export default function Dashboard() {
|
||||
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>
|
||||
<Label htmlFor="custom-uptime-check">{t('Applications.Add.CustomUptimeCheck')}</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
|
||||
{t('Applications.Add.CustomUptimeCheckTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{customUptimeCheck && (
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>Uptime Check URL</Label>
|
||||
<Label>{t('Applications.Add.UptimeCheckURL')}</Label>
|
||||
<Input
|
||||
placeholder="https://example.com/status"
|
||||
placeholder={t('Applications.Add.UptimeCheckURLPlaceholder')}
|
||||
value={uptimecheckUrl}
|
||||
onChange={(e) => setUptimecheckUrl(e.target.value)}
|
||||
/>
|
||||
@ -617,12 +604,12 @@ export default function Dashboard() {
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={add}
|
||||
disabled={!name || !publicURL || !serverId}
|
||||
>
|
||||
Add
|
||||
{t('Common.add')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -633,7 +620,7 @@ export default function Dashboard() {
|
||||
<div className="flex flex-col gap-2 mb-4 pt-2">
|
||||
<Input
|
||||
id="application-search"
|
||||
placeholder="Type to search..."
|
||||
placeholder={t('Applications.Search.Placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
@ -668,7 +655,7 @@ export default function Dashboard() {
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Icon</span>
|
||||
<span className="text-gray-500 text-xs">{t('Applications.Card.Icon')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
@ -698,7 +685,7 @@ export default function Dashboard() {
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Image</span>
|
||||
<span className="text-gray-500 text-xs">{t('Applications.Card.Image')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
@ -710,7 +697,7 @@ export default function Dashboard() {
|
||||
{app.description && (
|
||||
<br className="hidden md:block" />
|
||||
)}
|
||||
Server: {app.server || "No server"}
|
||||
{t('Applications.Card.Server')}: {app.server || t('Applications.Card.NoServer')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@ -727,7 +714,7 @@ export default function Dashboard() {
|
||||
}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Public URL
|
||||
{t('Applications.Card.PublicURL')}
|
||||
</Button>
|
||||
{app.localURL && (
|
||||
<Button
|
||||
@ -738,7 +725,7 @@ export default function Dashboard() {
|
||||
}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Local URL
|
||||
{t('Applications.Card.LocalURL')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -761,7 +748,7 @@ export default function Dashboard() {
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Edit Application
|
||||
@ -929,7 +916,7 @@ export default function Dashboard() {
|
||||
onClick={() => window.open(app.publicURL, "_blank")}
|
||||
>
|
||||
<Link className="h-4 w-4" />
|
||||
Public URL
|
||||
{t('Applications.Card.PublicURL')}
|
||||
</Button>
|
||||
{app.localURL && (
|
||||
<Button
|
||||
@ -938,7 +925,7 @@ export default function Dashboard() {
|
||||
onClick={() => window.open(app.localURL, "_blank")}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Local URL
|
||||
{t('Applications.Card.LocalURL')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -961,7 +948,7 @@ export default function Dashboard() {
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Edit Application
|
||||
@ -1124,7 +1111,7 @@ export default function Dashboard() {
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="inline-block" role="status" aria-label="loading">
|
||||
<svg
|
||||
className="w-6 h-6 stroke-white animate-spin "
|
||||
className="w-6 h-6 stroke-white animate-spin"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -1144,14 +1131,16 @@ export default function Dashboard() {
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
<span className="sr-only">{t('Common.Loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4 pb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"}
|
||||
{totalItems > 0
|
||||
? t('Applications.Pagination.Showing', { start: startItem, end: endItem, total: totalItems })
|
||||
: t('Applications.Pagination.NoApplications')}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
|
||||
@ -16,8 +16,10 @@ import {
|
||||
import { ReactFlow, Controls, Background, ConnectionLineType } from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations();
|
||||
const [nodes, setNodes] = useState<any[]>([]);
|
||||
const [edges, setEdges] = useState<any[]>([]);
|
||||
|
||||
@ -58,13 +60,13 @@ export default function Dashboard() {
|
||||
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="dark:text-slate-300">
|
||||
My Infrastructure
|
||||
{t('Network.Breadcrumb.MyInfrastructure')}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="dark:text-slate-300">
|
||||
Network
|
||||
{t('Network.Breadcrumb.Network')}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
|
||||
@ -22,6 +22,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import NextLink from "next/link"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
interface ServerHistory {
|
||||
labels: string[];
|
||||
@ -30,6 +31,8 @@ interface ServerHistory {
|
||||
ram: (number | null)[];
|
||||
disk: (number | null)[];
|
||||
online: (boolean | null)[];
|
||||
gpu: (number | null)[];
|
||||
temp: (number | null)[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +57,8 @@ interface Server {
|
||||
cpuUsage: number;
|
||||
ramUsage: number;
|
||||
diskUsage: number;
|
||||
gpuUsage: number;
|
||||
temp: number;
|
||||
history?: ServerHistory;
|
||||
port: number;
|
||||
uptime?: string;
|
||||
@ -65,6 +70,7 @@ interface GetServersResponse {
|
||||
}
|
||||
|
||||
export default function ServerDetail() {
|
||||
const t = useTranslations()
|
||||
const params = useParams()
|
||||
const serverId = params.server_id as string
|
||||
const [server, setServer] = useState<Server | null>(null)
|
||||
@ -75,6 +81,8 @@ export default function ServerDetail() {
|
||||
const cpuChartRef = { current: null as Chart | null }
|
||||
const ramChartRef = { current: null as Chart | null }
|
||||
const diskChartRef = { current: null as Chart | null }
|
||||
const gpuChartRef = { current: null as Chart | null }
|
||||
const tempChartRef = { current: null as Chart | null }
|
||||
|
||||
const fetchServerDetails = async () => {
|
||||
try {
|
||||
@ -105,6 +113,8 @@ export default function ServerDetail() {
|
||||
if (cpuChartRef.current) cpuChartRef.current.destroy();
|
||||
if (ramChartRef.current) ramChartRef.current.destroy();
|
||||
if (diskChartRef.current) diskChartRef.current.destroy();
|
||||
if (gpuChartRef.current) gpuChartRef.current.destroy();
|
||||
if (tempChartRef.current) tempChartRef.current.destroy();
|
||||
|
||||
// Wait for DOM to be ready
|
||||
const initTimer = setTimeout(() => {
|
||||
@ -206,7 +216,7 @@ export default function ServerDetail() {
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: 'CPU Usage',
|
||||
label: t('Common.Server.CPU') + ' ' + t('Common.Server.Usage'),
|
||||
data: history.datasets.cpu,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
@ -219,7 +229,7 @@ export default function ServerDetail() {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'CPU Usage History',
|
||||
text: t('Common.Server.CPU') + ' ' + t('Server.UsageHistory'),
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
@ -253,7 +263,7 @@ export default function ServerDetail() {
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: 'RAM Usage',
|
||||
label: t('Common.Server.RAM') + ' ' + t('Common.Server.Usage'),
|
||||
data: history.datasets.ram,
|
||||
borderColor: 'rgb(153, 102, 255)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||
@ -266,7 +276,7 @@ export default function ServerDetail() {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'RAM Usage History',
|
||||
text: t('Common.Server.RAM') + ' ' + t('Server.UsageHistory'),
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
@ -300,7 +310,7 @@ export default function ServerDetail() {
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: 'Disk Usage',
|
||||
label: t('Common.Server.Disk') + ' ' + t('Common.Server.Usage'),
|
||||
data: history.datasets.disk,
|
||||
borderColor: 'rgb(255, 159, 64)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||
@ -313,7 +323,7 @@ export default function ServerDetail() {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Disk Usage History',
|
||||
text: t('Common.Server.Disk') + ' ' + t('Server.UsageHistory'),
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
@ -339,6 +349,105 @@ export default function ServerDetail() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const gpuCanvas = document.getElementById(`gpu-chart`) as HTMLCanvasElement
|
||||
if (gpuCanvas) {
|
||||
gpuChartRef.current = new Chart(gpuCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: t('Common.Server.GPU') + ' ' + t('Common.Server.Usage'),
|
||||
data: history.datasets.gpu,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
fill: true,
|
||||
spanGaps: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: t('Common.Server.GPU') + ' ' + t('Common.Server.UsageHistory'),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const tempCanvas = document.getElementById(`temp-chart`) as HTMLCanvasElement
|
||||
if (tempCanvas) {
|
||||
tempChartRef.current = new Chart(tempCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: t('Common.Server.Temperature') + ' ' + t('Common.Server.Usage'),
|
||||
data: history.datasets.temp,
|
||||
borderColor: 'rgb(255, 159, 64)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||
fill: true,
|
||||
spanGaps: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: t('Common.Server.Temperature') + ' ' + t('Server.UsageHistory'),
|
||||
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,
|
||||
ticks: {
|
||||
callback: function(value: any) {
|
||||
return value + '°C';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
@ -346,6 +455,8 @@ export default function ServerDetail() {
|
||||
if (cpuChartRef.current) cpuChartRef.current.destroy();
|
||||
if (ramChartRef.current) ramChartRef.current.destroy();
|
||||
if (diskChartRef.current) diskChartRef.current.destroy();
|
||||
if (gpuChartRef.current) gpuChartRef.current.destroy();
|
||||
if (tempChartRef.current) tempChartRef.current.destroy();
|
||||
};
|
||||
}, [server, timeRange]);
|
||||
|
||||
@ -369,12 +480,12 @@ export default function ServerDetail() {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Servers.MyInfrastructure')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<NextLink href="/dashboard/servers" className="hover:underline">
|
||||
<BreadcrumbPage>Servers</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Servers.Title')}</BreadcrumbPage>
|
||||
</NextLink>
|
||||
</BreadcrumbItem>
|
||||
{server && (
|
||||
@ -415,7 +526,7 @@ export default function ServerDetail() {
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
<span className="sr-only">{t('Common.Loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : server ? (
|
||||
@ -431,9 +542,9 @@ export default function ServerDetail() {
|
||||
{server.name}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{server.os || "No OS specified"} • {server.isVM ? "Virtual Machine" : "Physical Server"}
|
||||
{server.os || t('Common.Server.OS')} • {server.isVM ? t('Server.VM') : t('Server.Physical')}
|
||||
{server.isVM && server.hostServer && (
|
||||
<> • Hosted on {server.hostedVMs?.[0]?.name}</>
|
||||
<> • {t('Server.HostedOn')} {server.hostedVMs?.[0]?.name}</>
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
@ -444,7 +555,7 @@ export default function ServerDetail() {
|
||||
<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}
|
||||
{t('Common.since', { date: server.uptime })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -453,25 +564,25 @@ export default function ServerDetail() {
|
||||
<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>
|
||||
<h3 className="text-sm font-medium">{t('Server.Hardware')}</h3>
|
||||
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||
<div className="text-muted-foreground">CPU:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.CPU')}:</div>
|
||||
<div>{server.cpu || "-"}</div>
|
||||
<div className="text-muted-foreground">GPU:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.GPU')}:</div>
|
||||
<div>{server.gpu || "-"}</div>
|
||||
<div className="text-muted-foreground">RAM:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.RAM')}:</div>
|
||||
<div>{server.ram || "-"}</div>
|
||||
<div className="text-muted-foreground">Disk:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.Disk')}:</div>
|
||||
<div>{server.disk || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Network</h3>
|
||||
<h3 className="text-sm font-medium">{t('Server.Network')}</h3>
|
||||
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||
<div className="text-muted-foreground">IP Address:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.IP')}:</div>
|
||||
<div>{server.ip || "-"}</div>
|
||||
<div className="text-muted-foreground">Management URL:</div>
|
||||
<div className="text-muted-foreground">{t('Server.ManagementURL')}:</div>
|
||||
<div>
|
||||
{server.url ? (
|
||||
<a href={server.url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 text-blue-500 hover:underline">
|
||||
@ -486,9 +597,9 @@ export default function ServerDetail() {
|
||||
|
||||
{server.monitoring && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Current Usage</h3>
|
||||
<h3 className="text-sm font-medium">{t('Server.CurrentUsage')}</h3>
|
||||
<div className="grid grid-cols-[120px_1fr] text-sm gap-1">
|
||||
<div className="text-muted-foreground">CPU Usage:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.CPU')}:</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
@ -496,9 +607,9 @@ export default function ServerDetail() {
|
||||
style={{ width: `${server.cpuUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>{server.cpuUsage}%</span>
|
||||
<span>{server.cpuUsage !== null && server.cpuUsage !== undefined ? `${server.cpuUsage}%` : t('Common.noData')}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">RAM Usage:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.RAM')}:</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
@ -506,9 +617,9 @@ export default function ServerDetail() {
|
||||
style={{ width: `${server.ramUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>{server.ramUsage}%</span>
|
||||
<span>{server.ramUsage !== null && server.ramUsage !== undefined ? `${server.ramUsage}%` : t('Common.noData')}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Disk Usage:</div>
|
||||
<div className="text-muted-foreground">{t('Common.Server.Disk')}:</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
@ -516,8 +627,36 @@ export default function ServerDetail() {
|
||||
style={{ width: `${server.diskUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>{server.diskUsage}%</span>
|
||||
<span>{server.diskUsage !== null && server.diskUsage !== undefined ? `${server.diskUsage}%` : t('Common.noData')}</span>
|
||||
</div>
|
||||
{server.gpuUsage && server.gpuUsage !== null && server.gpuUsage !== undefined && server.gpuUsage.toString() !== "0" && (
|
||||
<>
|
||||
<div className="text-muted-foreground">{t('Common.Server.GPU')}:</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.gpuUsage && server.gpuUsage > 80 ? "bg-destructive" : server.gpuUsage && server.gpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||
style={{ width: `${server.gpuUsage || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>{server.gpuUsage && server.gpuUsage !== null && server.gpuUsage !== undefined ? `${server.gpuUsage}%` : t('Common.noData')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{server.temp && server.temp !== null && server.temp !== undefined && server.temp.toString() !== "0" && (
|
||||
<>
|
||||
<div className="text-muted-foreground">{t('Common.Server.Temperature')}:</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.temp && server.temp > 80 ? "bg-destructive" : server.temp && server.temp > 60 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||
style={{ width: `${Math.min(server.temp || 0, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>{server.temp !== null && server.temp !== undefined && server.temp !== 0 ? `${server.temp}°C` : t('Common.noData')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -532,30 +671,30 @@ export default function ServerDetail() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Resource Usage History</CardTitle>
|
||||
<CardTitle>{t('Server.ResourceUsageHistory')}</CardTitle>
|
||||
<CardDescription>
|
||||
{timeRange === '1h'
|
||||
? 'Last hour, per minute'
|
||||
? t('Server.TimeRange.LastHour')
|
||||
: timeRange === '1d'
|
||||
? 'Last 24 hours, 15-minute intervals'
|
||||
? t('Server.TimeRange.Last24Hours')
|
||||
: timeRange === '7d'
|
||||
? 'Last 7 days, hourly intervals'
|
||||
: 'Last 30 days, 4-hour intervals'}
|
||||
? t('Server.TimeRange.Last7Days')
|
||||
: t('Server.TimeRange.Last30Days')}
|
||||
</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" />
|
||||
<SelectValue placeholder={t('Server.TimeRange.Select')} />
|
||||
</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>
|
||||
<SelectItem value="1h">{t('Server.TimeRange.LastHour')}</SelectItem>
|
||||
<SelectItem value="1d">{t('Server.TimeRange.Last24Hours')}</SelectItem>
|
||||
<SelectItem value="7d">{t('Server.TimeRange.Last7Days')}</SelectItem>
|
||||
<SelectItem value="30d">{t('Server.TimeRange.Last30Days')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={refreshData}>Refresh</Button>
|
||||
<Button variant="outline" onClick={refreshData}>{t('Common.Refresh')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -570,6 +709,16 @@ export default function ServerDetail() {
|
||||
<div className="h-[200px] relative bg-background">
|
||||
<canvas id="disk-chart" />
|
||||
</div>
|
||||
{server.history?.datasets.gpu.some(value => value !== null && value !== 0) && (
|
||||
<div className="h-[200px] relative bg-background">
|
||||
<canvas id="gpu-chart" />
|
||||
</div>
|
||||
)}
|
||||
{server.history?.datasets.temp.some(value => value !== null && value !== 0) && (
|
||||
<div className="h-[200px] relative bg-background">
|
||||
<canvas id="temp-chart" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -580,8 +729,8 @@ export default function ServerDetail() {
|
||||
{server.hostedVMs && server.hostedVMs.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Virtual Machines</CardTitle>
|
||||
<CardDescription>Virtual machines hosted on this server</CardDescription>
|
||||
<CardTitle>{t('Server.VirtualMachines')}</CardTitle>
|
||||
<CardDescription>{t('Server.VirtualMachinesDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -610,7 +759,7 @@ export default function ServerDetail() {
|
||||
<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}
|
||||
{t('Common.since', { date: hostedVM.uptime })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -625,43 +774,43 @@ export default function ServerDetail() {
|
||||
<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 || "-"}
|
||||
<b>{t('Common.Server.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"}
|
||||
<b>{t('Common.Server.IP')}:</b> {hostedVM.ip || t('Common.notSet')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full mb-2">
|
||||
<h4 className="text-sm font-semibold">Hardware Information</h4>
|
||||
<h4 className="text-sm font-semibold">{t('Server.HardwareInformation')}</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 || "-"}
|
||||
<b>{t('Common.Server.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 || "-"}
|
||||
<b>{t('Common.Server.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 || "-"}
|
||||
<b>{t('Common.Server.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 || "-"}
|
||||
<b>{t('Common.Server.Disk')}:</b> {hostedVM.disk || "-"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -676,7 +825,7 @@ export default function ServerDetail() {
|
||||
<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>
|
||||
<span className="text-sm font-medium">{t('Common.Server.CPU')}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium">
|
||||
{hostedVM.cpuUsage || 0}%
|
||||
@ -694,7 +843,7 @@ export default function ServerDetail() {
|
||||
<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>
|
||||
<span className="text-sm font-medium">{t('Common.Server.RAM')}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium">
|
||||
{hostedVM.ramUsage || 0}%
|
||||
@ -712,7 +861,7 @@ export default function ServerDetail() {
|
||||
<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>
|
||||
<span className="text-sm font-medium">{t('Common.Server.Disk')}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium">
|
||||
{hostedVM.diskUsage || 0}%
|
||||
@ -737,8 +886,8 @@ export default function ServerDetail() {
|
||||
</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>
|
||||
<h2 className="text-2xl font-bold">{t('Server.NotFound')}</h2>
|
||||
<p className="text-muted-foreground mt-2">{t('Server.NotFoundDescription')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -19,9 +19,10 @@ 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 { AlertCircle, Check, Palette, User, Bell, AtSign, Send, MessageSquare, Trash2, Play, Languages } from "lucide-react"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { toast } from "sonner"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -35,7 +36,7 @@ import {
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
interface NotificationsResponse {
|
||||
notifications: any[]
|
||||
@ -46,6 +47,7 @@ interface NotificationResponse {
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const t = useTranslations()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
const [email, setEmail] = useState<string>("")
|
||||
@ -80,7 +82,8 @@ export default function Settings() {
|
||||
const [pushoverUrl, setPushoverUrl] = useState<string>("")
|
||||
const [pushoverToken, setPushoverToken] = useState<string>("")
|
||||
const [pushoverUser, setPushoverUser] = useState<string>("")
|
||||
|
||||
const [echobellURL, setEchobellURL] = useState<string>("")
|
||||
const [language, setLanguage] = useState<string>("english")
|
||||
const [notifications, setNotifications] = useState<any[]>([])
|
||||
|
||||
const [notificationTextApplication, setNotificationTextApplication] = useState<string>("")
|
||||
@ -92,7 +95,7 @@ export default function Settings() {
|
||||
setEmailError("")
|
||||
|
||||
if (!email) {
|
||||
setEmailError("Email is required")
|
||||
setEmailError(t('Settings.UserSettings.ChangeEmail.EmailRequired'))
|
||||
setEmailErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setEmailErrorVisible(false)
|
||||
@ -123,7 +126,7 @@ export default function Settings() {
|
||||
const changePassword = async () => {
|
||||
try {
|
||||
if (password !== confirmPassword) {
|
||||
setPasswordError("Passwords do not match")
|
||||
setPasswordError(t('Settings.UserSettings.ChangePassword.PasswordsDontMatch'))
|
||||
setPasswordErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setPasswordErrorVisible(false)
|
||||
@ -132,7 +135,7 @@ export default function Settings() {
|
||||
return
|
||||
}
|
||||
if (!oldPassword || !password || !confirmPassword) {
|
||||
setPasswordError("All fields are required")
|
||||
setPasswordError(t('Settings.UserSettings.ChangePassword.AllFieldsRequired'))
|
||||
setPasswordErrorVisible(true)
|
||||
setTimeout(() => {
|
||||
setPasswordErrorVisible(false)
|
||||
@ -188,6 +191,7 @@ export default function Settings() {
|
||||
pushoverUrl: pushoverUrl,
|
||||
pushoverToken: pushoverToken,
|
||||
pushoverUser: pushoverUser,
|
||||
echobellURL: echobellURL,
|
||||
})
|
||||
getNotifications()
|
||||
} catch (error: any) {
|
||||
@ -263,12 +267,32 @@ export default function Settings() {
|
||||
const response = await axios.post("/api/notifications/test", {
|
||||
notificationId: id,
|
||||
})
|
||||
toast.success("Notification will be sent in a few seconds.")
|
||||
toast.success(t('Settings.Notifications.TestSuccess'))
|
||||
} catch (error: any) {
|
||||
toast.error(error.response.data.error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const language = Cookies.get("language")
|
||||
if (language === "en") {
|
||||
setLanguage("english")
|
||||
} else if (language === "de") {
|
||||
setLanguage("german")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setLanguageFunc = (value: string) => {
|
||||
setLanguage(value)
|
||||
if (value === "english") {
|
||||
Cookies.set("language", "en")
|
||||
} else if (value === "german") {
|
||||
Cookies.set("language", "de")
|
||||
}
|
||||
// Reload the page
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
@ -280,15 +304,11 @@ export default function Settings() {
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbPage>/</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Settings.Breadcrumb.Dashboard')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Dashboard</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Settings</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Settings.Breadcrumb.Settings')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
@ -296,31 +316,31 @@ export default function Settings() {
|
||||
</header>
|
||||
<div className="p-6">
|
||||
<div className="pb-4">
|
||||
<span className="text-3xl font-bold">Settings</span>
|
||||
<span className="text-3xl font-bold">{t('Settings.Title')}</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>
|
||||
<h2 className="text-xl font-semibold">{t('Settings.UserSettings.Title')}</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.
|
||||
{t('Settings.UserSettings.Description')}
|
||||
</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>
|
||||
<h3 className="font-semibold text-lg">{t('Settings.UserSettings.ChangeEmail.Title')}</h3>
|
||||
</div>
|
||||
|
||||
{emailErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertTitle>{t('Common.Error')}</AlertTitle>
|
||||
<AlertDescription>{emailError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@ -328,34 +348,33 @@ export default function Settings() {
|
||||
{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>
|
||||
<AlertTitle>{t('Settings.UserSettings.ChangeEmail.Success')}</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter new email"
|
||||
placeholder={t('Settings.UserSettings.ChangeEmail.Placeholder')}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changeEmail} className="w-full h-11">
|
||||
Change Email
|
||||
{t('Settings.UserSettings.ChangeEmail.Button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="font-semibold text-lg">Change Password</h3>
|
||||
<h3 className="font-semibold text-lg">{t('Settings.UserSettings.ChangePassword.Title')}</h3>
|
||||
</div>
|
||||
|
||||
{passwordErrorVisible && (
|
||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertTitle>{t('Common.Error')}</AlertTitle>
|
||||
<AlertDescription>{passwordError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@ -363,35 +382,34 @@ export default function Settings() {
|
||||
{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>
|
||||
<AlertTitle>{t('Settings.UserSettings.ChangePassword.Success')}</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter old password"
|
||||
placeholder={t('Settings.UserSettings.ChangePassword.OldPassword')}
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
placeholder={t('Settings.UserSettings.ChangePassword.NewPassword')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t('Settings.UserSettings.ChangePassword.ConfirmPassword')}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button onClick={changePassword} className="w-full h-11">
|
||||
Change Password
|
||||
{t('Settings.UserSettings.ChangePassword.Button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -403,25 +421,53 @@ export default function Settings() {
|
||||
<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>
|
||||
<h2 className="text-xl font-semibold">{t('Settings.ThemeSettings.Title')}</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.
|
||||
{t('Settings.ThemeSettings.Description')}
|
||||
</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)}
|
||||
{t(`Settings.ThemeSettings.${(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>
|
||||
<SelectItem value="light">{t('Settings.ThemeSettings.Light')}</SelectItem>
|
||||
<SelectItem value="dark">{t('Settings.ThemeSettings.Dark')}</SelectItem>
|
||||
<SelectItem value="system">{t('Settings.ThemeSettings.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-2">
|
||||
<Languages className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-xl font-semibold">{t('Settings.LanguageSettings.Title')}</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
{t('Settings.LanguageSettings.Description')}
|
||||
</div>
|
||||
|
||||
<div className="max-w-md">
|
||||
<Select value={language} onValueChange={(value: string) => setLanguageFunc(value)}>
|
||||
<SelectTrigger className="w-full h-11">
|
||||
<SelectValue>
|
||||
{t(`Settings.LanguageSettings.${(language ?? "english").charAt(0).toUpperCase() + (language ?? "english").slice(1)}`)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">{t('Settings.LanguageSettings.English')}</SelectItem>
|
||||
<SelectItem value="german">{t('Settings.LanguageSettings.German')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -434,61 +480,60 @@ export default function Settings() {
|
||||
<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>
|
||||
<h2 className="text-xl font-semibold">{t('Settings.Notifications.Title')}</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.
|
||||
{t('Settings.Notifications.Description')}
|
||||
</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
|
||||
{t('Settings.Notifications.AddChannel')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Add Notification</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('Settings.Notifications.AddNotification.Title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
id="notificationName"
|
||||
placeholder="Notification Name (optional)"
|
||||
placeholder={t('Settings.Notifications.AddNotification.Name')}
|
||||
onChange={(e) => setNotificationName(e.target.value)}
|
||||
/>
|
||||
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Notification Type" />
|
||||
<SelectValue placeholder={t('Settings.Notifications.AddNotification.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>
|
||||
<SelectItem value="smtp">{t('Settings.Notifications.AddNotification.SMTP.Title')}</SelectItem>
|
||||
<SelectItem value="telegram">{t('Settings.Notifications.AddNotification.Telegram.Title')}</SelectItem>
|
||||
<SelectItem value="discord">{t('Settings.Notifications.AddNotification.Discord.Title')}</SelectItem>
|
||||
<SelectItem value="gotify">{t('Settings.Notifications.AddNotification.Gotify.Title')}</SelectItem>
|
||||
<SelectItem value="ntfy">{t('Settings.Notifications.AddNotification.Ntfy.Title')}</SelectItem>
|
||||
<SelectItem value="pushover">{t('Settings.Notifications.AddNotification.Pushover.Title')}</SelectItem>
|
||||
<SelectItem value="echobell">{t('Settings.Notifications.AddNotification.Echobell.Title')}</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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.SMTP.Port')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="smtpPort"
|
||||
placeholder="587"
|
||||
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
||||
/>
|
||||
@ -498,26 +543,24 @@ export default function Settings() {
|
||||
<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)
|
||||
{t('Settings.Notifications.AddNotification.SMTP.Secure')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtpUser">SMTP Username</Label>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.SMTP.Password')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
id="smtpPass"
|
||||
placeholder="••••••••"
|
||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||
/>
|
||||
@ -525,20 +568,18 @@ export default function Settings() {
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtpFrom">From Address</Label>
|
||||
<Label>{t('Settings.Notifications.AddNotification.SMTP.From')}</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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.SMTP.To')}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="smtpTo"
|
||||
placeholder="admin@example.com"
|
||||
onChange={(e) => setSmtpTo(e.target.value)}
|
||||
/>
|
||||
@ -551,20 +592,16 @@ export default function Settings() {
|
||||
{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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Telegram.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Telegram.ChatId')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="telegramChatId"
|
||||
placeholder=""
|
||||
onChange={(e) => setTelegramChatId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -574,11 +611,9 @@ export default function Settings() {
|
||||
{notificationType === "discord" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="discordWebhook">Webhook URL</Label>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Discord.Webhook')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="discordWebhook"
|
||||
placeholder=""
|
||||
onChange={(e) => setDiscordWebhook(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -588,19 +623,15 @@ export default function Settings() {
|
||||
{notificationType === "gotify" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="gotifyUrl">Gotify URL</Label>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Gotify.Token')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="gotifyToken"
|
||||
placeholder=""
|
||||
onChange={(e) => setGotifyToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -611,19 +642,15 @@ export default function Settings() {
|
||||
{notificationType === "ntfy" && (
|
||||
<div className="mt-4">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Ntfy.Token')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="ntfyToken"
|
||||
placeholder=""
|
||||
onChange={(e) => setNtfyToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -634,42 +661,51 @@ export default function Settings() {
|
||||
{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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.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>
|
||||
<Label>{t('Settings.Notifications.AddNotification.Pushover.User')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="pushoverUser"
|
||||
placeholder="e.g. 1234567890"
|
||||
onChange={(e) => setPushoverUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationType === "echobell" && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label>{t('Settings.Notifications.AddNotification.Echobell.Url')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. https://hook.echobell.one/t/xxx"
|
||||
onChange={(e) => setEchobellURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{t('Settings.Notifications.AddNotification.Echobell.AddMessage')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Select>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
|
||||
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={addNotification}>{t('Common.add')}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@ -677,65 +713,63 @@ export default function Settings() {
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button className="w-full h-11" variant="outline">
|
||||
Customize Notification Text
|
||||
{t('Settings.Notifications.CustomizeText.Display')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('Settings.Notifications.CustomizeText.Title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="text_application">Notification Text for Applications</Label>
|
||||
<Label>{t('Settings.Notifications.CustomizeText.Application')}</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>
|
||||
<Label>{t('Settings.Notifications.CustomizeText.Server')}</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>
|
||||
<div className="pt-4 text-sm text-muted-foreground">
|
||||
{t('Settings.Notifications.CustomizeText.Placeholders.Title')}
|
||||
<ul className="list-disc list-inside space-y-1 pt-2">
|
||||
<li>
|
||||
<b>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Title')}</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>
|
||||
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Name')}</li>
|
||||
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Status')}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Application related:</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Title')}</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>
|
||||
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Name')}</li>
|
||||
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Url')}</li>
|
||||
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Status')}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
|
||||
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={editNotificationText}>
|
||||
{t('Common.Save')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-medium mb-4">Active Notification Channels</h3>
|
||||
<h3 className="text-lg font-medium mb-4">{t('Settings.Notifications.ActiveChannels')}</h3>
|
||||
<div className="space-y-3">
|
||||
{notifications.length > 0 ? (
|
||||
notifications.map((notification) => (
|
||||
@ -775,14 +809,12 @@ export default function Settings() {
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium capitalize">{notification.name && notification.name !== "" ? notification.name : notification.type}</h3>
|
||||
<h3 className="font-medium capitalize">
|
||||
{notification.name ||
|
||||
t(`Settings.Notifications.AddNotification.${notification.type.charAt(0).toUpperCase() + notification.type.slice(1)}.Title`)}
|
||||
</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"}
|
||||
{t(`Settings.Notifications.AddNotification.${notification.type.charAt(0).toUpperCase() + notification.type.slice(1)}.Description`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -794,7 +826,7 @@ export default function Settings() {
|
||||
onClick={() => testNotification(notification.id)}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-1" />
|
||||
Test
|
||||
{t('Settings.Notifications.Test')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -803,6 +835,7 @@ export default function Settings() {
|
||||
onClick={() => deleteNotification(notification.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
{t('Settings.Notifications.Delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -814,9 +847,11 @@ export default function Settings() {
|
||||
<Bell className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-1">No notifications configured</h3>
|
||||
<h3 className="text-lg font-medium mb-1">
|
||||
{t('Settings.Notifications.NoNotifications')}
|
||||
</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.
|
||||
{t('Settings.Notifications.NoNotificationsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -31,6 +31,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const timeFormats = {
|
||||
1: (timestamp: string) =>
|
||||
@ -81,6 +82,7 @@ interface PaginationData {
|
||||
}
|
||||
|
||||
export default function Uptime() {
|
||||
const t = useTranslations();
|
||||
const [data, setData] = useState<UptimeData[]>([]);
|
||||
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
|
||||
const [pagination, setPagination] = useState<PaginationData>({
|
||||
@ -138,34 +140,30 @@ export default function Uptime() {
|
||||
};
|
||||
|
||||
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");
|
||||
toast.error(t('Uptime.Messages.NumberValidation'));
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||
|
||||
setItemsPerPage(validatedValue);
|
||||
setPagination(prev => ({...prev, currentPage: 1})); // Reset to first page
|
||||
setPagination(prev => ({...prev, currentPage: 1}));
|
||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||
expires: 365,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
// Fetch data with new pagination
|
||||
getData(timespan, 1, validatedValue);
|
||||
}, 300); // 300ms delay
|
||||
}, 300);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -187,11 +185,11 @@ export default function Uptime() {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Uptime.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Uptime</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t('Uptime.Breadcrumb.Uptime')}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
@ -200,7 +198,7 @@ export default function Uptime() {
|
||||
<Toaster />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-3xl font-bold">Uptime</span>
|
||||
<span className="text-3xl font-bold">{t('Uptime.Title')}</span>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={String(itemsPerPage)}
|
||||
@ -213,22 +211,22 @@ export default function Uptime() {
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
||||
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||
<SelectItem value={String(itemsPerPage)}>
|
||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
||||
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.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>
|
||||
<SelectItem value="5">{t('Common.ItemsPerPage.5')}</SelectItem>
|
||||
<SelectItem value="10">{t('Common.ItemsPerPage.10')}</SelectItem>
|
||||
<SelectItem value="15">{t('Common.ItemsPerPage.15')}</SelectItem>
|
||||
<SelectItem value="20">{t('Common.ItemsPerPage.20')}</SelectItem>
|
||||
<SelectItem value="25">{t('Common.ItemsPerPage.25')}</SelectItem>
|
||||
<div className="p-2 border-t mt-1">
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
||||
<Label htmlFor="custom-items" className="text-xs font-medium">{t('Common.ItemsPerPage.Custom')}</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
id="custom-items"
|
||||
@ -239,8 +237,6 @@ export default function Uptime() {
|
||||
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");
|
||||
@ -249,7 +245,6 @@ export default function Uptime() {
|
||||
}
|
||||
}}
|
||||
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);
|
||||
@ -257,7 +252,6 @@ export default function Uptime() {
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Clear any existing debounce timer to apply immediately
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
@ -265,7 +259,6 @@ export default function Uptime() {
|
||||
|
||||
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}));
|
||||
@ -275,15 +268,13 @@ export default function Uptime() {
|
||||
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>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">{t('Common.ItemsPerPage.items')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectContent>
|
||||
@ -297,13 +288,13 @@ export default function Uptime() {
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select timespan" />
|
||||
<SelectValue placeholder={t('Uptime.TimeRange.Select')} />
|
||||
</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>
|
||||
<SelectItem value="1">{t('Uptime.TimeRange.LastHour')}</SelectItem>
|
||||
<SelectItem value="2">{t('Uptime.TimeRange.LastDay')}</SelectItem>
|
||||
<SelectItem value="3">{t('Uptime.TimeRange.Last7Days')}</SelectItem>
|
||||
<SelectItem value="4">{t('Uptime.TimeRange.Last30Days')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -311,7 +302,7 @@ export default function Uptime() {
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
<div className="text-center py-8">{t('Uptime.Messages.Loading')}</div>
|
||||
) : (
|
||||
data.map((app) => {
|
||||
const reversedSummary = [...app.uptimeSummary].reverse();
|
||||
@ -370,10 +361,10 @@ export default function Uptime() {
|
||||
</p>
|
||||
<p>
|
||||
{entry.missing
|
||||
? "No data"
|
||||
? t('Uptime.Status.NoData')
|
||||
: entry.online
|
||||
? "Online"
|
||||
: "Offline"}
|
||||
? t('Uptime.Status.Online')
|
||||
: t('Uptime.Status.Offline')}
|
||||
</p>
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-gray-900" />
|
||||
@ -397,8 +388,12 @@ export default function Uptime() {
|
||||
<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"}
|
||||
? t('Uptime.Pagination.Showing', {
|
||||
start: ((pagination.currentPage - 1) * itemsPerPage) + 1,
|
||||
end: Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems),
|
||||
total: pagination.totalItems
|
||||
})
|
||||
: t('Uptime.Messages.NoItems')}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination>
|
||||
|
||||
@ -2,6 +2,9 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import {NextIntlClientProvider} from 'next-intl';
|
||||
import {getLocale} from 'next-intl/server';
|
||||
import {cookies} from 'next/headers';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -18,13 +21,17 @@ export const metadata: Metadata = {
|
||||
description: "The only Dashboard you will need for your Services",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const locale = cookieStore.get('language')?.value || 'en';
|
||||
const messages = (await import(`@/i18n/languages/${locale}.json`)).default;
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang={locale}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
@ -34,7 +41,9 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
16
app/page.tsx
@ -14,8 +14,10 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations('Home');
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [rememberMe, setRememberMe] = useState(false)
|
||||
@ -75,20 +77,20 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-foreground">CoreControl</h1>
|
||||
<p className="text-muted-foreground">Sign in to access your dashboard</p>
|
||||
<p className="text-muted-foreground">{t('LoginCardDescription')}</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>
|
||||
<CardTitle className="text-2xl font-semibold">{t('LoginCardTitle')}</CardTitle>
|
||||
<CardDescription>{t('LoginCardDescription')}</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>
|
||||
<AlertTitle>{t('AuthenticationError')}</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@ -96,7 +98,7 @@ export default function Home() {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
{t('Email')}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
@ -116,7 +118,7 @@ export default function Home() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-sm font-medium">
|
||||
Password
|
||||
{t('Password')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@ -138,7 +140,7 @@ export default function Home() {
|
||||
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button className="w-full" onClick={login} disabled={isLoading}>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
{isLoading ? t('SigninButtonSigningIn') : t('SigninButton')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -37,7 +37,7 @@ import { useRouter } from "next/navigation"
|
||||
import packageJson from "@/package.json"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
interface NavItem {
|
||||
title: string
|
||||
icon?: React.ComponentType<any>
|
||||
@ -46,49 +46,52 @@ interface NavItem {
|
||||
items?: NavItem[]
|
||||
}
|
||||
|
||||
const data: { navMain: NavItem[] } = {
|
||||
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const t = useTranslations('Sidebar')
|
||||
const data: { navMain: NavItem[] } = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
title: t('Dashboard'),
|
||||
icon: LayoutDashboardIcon,
|
||||
url: "/dashboard",
|
||||
},
|
||||
{
|
||||
title: "My Infrastructure",
|
||||
title: t('My Infrastructure'),
|
||||
url: "#",
|
||||
icon: Briefcase,
|
||||
items: [
|
||||
{
|
||||
title: "Servers",
|
||||
title: t('Servers'),
|
||||
icon: Server,
|
||||
url: "/dashboard/servers",
|
||||
},
|
||||
{
|
||||
title: "Applications",
|
||||
title: t('Applications'),
|
||||
icon: AppWindow,
|
||||
url: "/dashboard/applications",
|
||||
},
|
||||
{
|
||||
title: "Uptime",
|
||||
title: t('Uptime'),
|
||||
icon: Activity,
|
||||
url: "/dashboard/uptime",
|
||||
},
|
||||
{
|
||||
title: "Network",
|
||||
title: t('Network'),
|
||||
icon: Network,
|
||||
url: "/dashboard/network",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
title: t('Settings'),
|
||||
icon: Settings,
|
||||
url: "/dashboard/settings",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
@ -115,7 +118,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild className="gap-3">
|
||||
<a href="https://github.com/crocofied/corecontrol" className="transition-all hover:opacity-80">
|
||||
<a href="https://github.com/crocofied/corecontrol" target="_blank" rel="noreferrer noopener" 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>
|
||||
@ -132,7 +135,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarContent className="flex flex-col h-full py-4">
|
||||
<SidebarGroup className="flex-grow">
|
||||
<SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2">
|
||||
Main Navigation
|
||||
{t('Main Navigation')}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
@ -197,7 +200,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
onClick={logout}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Logout
|
||||
{t('Logout')}
|
||||
</Button>
|
||||
</SidebarFooter>
|
||||
</SidebarContent>
|
||||
|
||||
@ -54,6 +54,7 @@ export default defineConfig({
|
||||
{ text: 'Gotify', link: '/notifications/Gotify' },
|
||||
{ text: 'Ntfy', link: '/notifications/Ntfy' },
|
||||
{ text: 'Pushover', link: '/notifications/Pushover' },
|
||||
{ text: 'Echobell', link: '/notifications/Echobell' },
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
4
docs/.vitepress/dist/404.html
vendored
@ -8,8 +8,8 @@
|
||||
<meta name="generator" content="VitePress v1.6.3">
|
||||
<link rel="preload stylesheet" href="/assets/style.DEOyzpKL.css" as="style">
|
||||
<link rel="preload stylesheet" href="/vp-icons.css" as="style">
|
||||
<script type="module" src="/assets/chunks/metadata.d21683cf.js"></script>
|
||||
<script type="module" src="/assets/app.DiWcjlN4.js"></script>
|
||||
<script type="module" src="/assets/chunks/metadata.80a3dac1.js"></script>
|
||||
<script type="module" src="/assets/app.C_TDNGCa.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
<script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
|
||||
|
||||
@ -1 +1 @@
|
||||
import{t as p}from"./chunks/theme.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};
|
||||
import{t as p}from"./chunks/theme.BsFEzhuB.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.DPDPlp3K.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp};
|
||||
1
docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.WKQwg8wp.js
vendored
Normal file
1
docs/.vitepress/dist/assets/chunks/metadata.80a3dac1.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\":\"DG8ZT4OR\",\"general_uptime.md\":\"CKBdQg4u\",\"index.md\":\"vIfS0_LS\",\"installation.md\":\"RudnHaMh\",\"notifications_discord.md\":\"D5alp298\",\"notifications_echobell.md\":\"IszWXk9P\",\"notifications_email.md\":\"n24Ra-lu\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"D36rLkt7\",\"notifications_ntfy.md\":\"BPwrZ9j5\",\"notifications_pushover.md\":\"B37wP4uj\",\"notifications_telegram.md\":\"B9HZvnCz\"}");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\"},{\"text\":\"Echobell\",\"link\":\"/notifications/Echobell\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");
|
||||
@ -1 +0,0 @@
|
||||
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/general_Settings.md.DG8ZT4OR.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as a,c as n,o as s,ag as i}from"./chunks/framework.DPDPlp3K.js";const o="/assets/settings_user.eib6RZVK.png",r="/assets/settings_theme.AZP0Uw0g.png",g="/assets/settings_language.CCbF4jzs.png",S=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745962518000}'),l={name:"general/Settings.md"};function h(c,t,p,d,u,m){return s(),n("div",null,t[0]||(t[0]=[i('<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="'+o+'" alt="User Settings"></p><p>You can change your email and password in the user settings. Please note that you need your old password to change your password.</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="'+r+'" alt="Theme Settings"></p><p>With the theme settings you have the choice between light and dark mode. There is also the option to select “System”, where the system settings are applied.</p><h2 id="language-settings" tabindex="-1">Language Settings <a class="header-anchor" href="#language-settings" aria-label="Permalink to "Language Settings""></a></h2><p><img src="'+g+'" alt="Language Setting"></p><p>To promote internationalization (also often known as i18n), you can select the language in which you want everything to be displayed within CoreControl. Currently there is the standard language “English” and the language German.</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><p>To receive notifications from CoreControl, you can add all your notification providers here. You can also customize the notification text.</p>',14)]))}const y=a(l,[["render",h]]);export{S as __pageData,y as default};
|
||||
1
docs/.vitepress/dist/assets/general_Settings.md.DG8ZT4OR.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as e}from"./chunks/settings_notifications.DL7eQG4d.js";import{_ as a,c as n,o as s,ag as i}from"./chunks/framework.DPDPlp3K.js";const o="/assets/settings_user.eib6RZVK.png",r="/assets/settings_theme.AZP0Uw0g.png",g="/assets/settings_language.CCbF4jzs.png",S=JSON.parse('{"title":"Settings","description":"","frontmatter":{},"headers":[],"relativePath":"general/Settings.md","filePath":"general/Settings.md","lastUpdated":1745962518000}'),l={name:"general/Settings.md"};function h(c,t,p,d,u,m){return s(),n("div",null,t[0]||(t[0]=[i("",14)]))}const y=a(l,[["render",h]]);export{S as __pageData,y as default};
|
||||
@ -1 +0,0 @@
|
||||
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 +0,0 @@
|
||||
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 +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};
|
||||
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":1745614668000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default};
|
||||
@ -1 +1 @@
|
||||
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed 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};
|
||||
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":1745614668000}'),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,4 +1,4 @@
|
||||
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>
|
||||
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":1745961699000}'),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><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>
|
||||
@ -33,4 +33,4 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.DPDPlp3K.js";const d
|
||||
<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};
|
||||
<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>`,13)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};
|
||||
@ -1 +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};
|
||||
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":1745961699000}'),l={name:"installation.md"};function e(p,s,h,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t("",13)]))}const g=i(l,[["render",e]]);export{d as __pageData,g as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as o,c as a,o as e,j as t,a as i}from"./chunks/framework.DPDPlp3K.js";const r="/assets/notifications_discord.BzLLVI_K.png",D=JSON.parse('{"title":"Discord","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745241280000}'),c={name:"notifications/Discord.md"};function d(n,s,l,p,f,_){return e(),a("div",null,s[0]||(s[0]=[t("h1",{id:"discord",tabindex:"-1"},[i("Discord "),t("a",{class:"header-anchor",href:"#discord","aria-label":'Permalink to "Discord"'},"")],-1),t("p",null,[t("img",{src:r,alt:"Discord"})],-1)]))}const h=o(c,[["render",d]]);export{D as __pageData,h as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Discord.md.D5alp298.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as i,c as a,o as s,j as e,a as t}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Discord Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745963141000}'),r={name:"notifications/Discord.md"};function n(c,o,d,l,f,p){return s(),a("div",null,o[0]||(o[0]=[e("h1",{id:"discord-notification-setup",tabindex:"-1"},[t("Discord Notification Setup "),e("a",{class:"header-anchor",href:"#discord-notification-setup","aria-label":'Permalink to "Discord Notification Setup"'},"")],-1),e("p",null,[t("To enable Discord notifications, you will need a "),e("strong",null,"Discord Webhook URL"),t("."),e("br"),t(" This URL allows the system to send messages directly to a specific Discord channel.")],-1),e("p",null,[t("You can create a webhook by following this "),e("a",{href:"https://support.discord.com/hc/articles/228383668",target:"_blank",rel:"noreferrer"},"official Discord guide"),t("."),e("br"),t(" Once created, simply paste the webhook URL into the designated field in your notification settings.")],-1)]))}const m=i(r,[["render",n]]);export{u as __pageData,m as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Discord.md.D5alp298.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as i,c as a,o as s,j as e,a as t}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Discord Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Discord.md","filePath":"notifications/Discord.md","lastUpdated":1745963141000}'),r={name:"notifications/Discord.md"};function n(c,o,d,l,f,p){return s(),a("div",null,o[0]||(o[0]=[e("h1",{id:"discord-notification-setup",tabindex:"-1"},[t("Discord Notification Setup "),e("a",{class:"header-anchor",href:"#discord-notification-setup","aria-label":'Permalink to "Discord Notification Setup"'},"")],-1),e("p",null,[t("To enable Discord notifications, you will need a "),e("strong",null,"Discord Webhook URL"),t("."),e("br"),t(" This URL allows the system to send messages directly to a specific Discord channel.")],-1),e("p",null,[t("You can create a webhook by following this "),e("a",{href:"https://support.discord.com/hc/articles/228383668",target:"_blank",rel:"noreferrer"},"official Discord guide"),t("."),e("br"),t(" Once created, simply paste the webhook URL into the designated field in your notification settings.")],-1)]))}const m=i(r,[["render",n]]);export{u as __pageData,m as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Echobell.md.IszWXk9P.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as e,c as t,o as n,ag as i}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Echobell Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Echobell.md","filePath":"notifications/Echobell.md","lastUpdated":1745964106000}'),l={name:"notifications/Echobell.md"};function a(s,o,r,c,h,d){return n(),t("div",null,o[0]||(o[0]=[i('<h1 id="echobell-notification-setup" tabindex="-1">Echobell Notification Setup <a class="header-anchor" href="#echobell-notification-setup" aria-label="Permalink to "Echobell Notification Setup""></a></h1><p>To enable Echobell notifications, you need the following:</p><ul><li><p><strong>Echobell Webhook URL</strong><br> The HTTP POST endpoint that Echobell exposes for your channel. You’ll find it in your channel’s <strong>Integrations → Webhooks</strong> section.</p></li><li><p><strong>Message Field Key</strong><br> The JSON field name that Echobell expects for the notification text. By default this is <code>message</code>, but you can verify or customize it under <strong>Integrations → Webhooks → Payload Settings</strong>.</p></li></ul><h2 id="how-to-get-your-webhook-url-and-field-key" tabindex="-1">How to get your Webhook URL and field key <a class="header-anchor" href="#how-to-get-your-webhook-url-and-field-key" aria-label="Permalink to "How to get your Webhook URL and field key""></a></h2><ol><li><strong>Log in</strong> to your Echobell account.</li><li><strong>Select the channel</strong> you want to send notifications to.</li><li>Navigate to <strong>Integrations → Webhooks</strong>. <ul><li>Copy the <strong>Webhook URL</strong> shown there (e.g., <code>https://api.echobell.one/hooks/abc123</code>).</li></ul></li><li>In the same screen, check <strong>Payload Settings</strong> and confirm the <strong>Field Key</strong> for your message (<code>message</code>).</li></ol>',5)]))}const f=e(l,[["render",a]]);export{u as __pageData,f as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Echobell.md.IszWXk9P.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as e,c as t,o as n,ag as i}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"Echobell Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Echobell.md","filePath":"notifications/Echobell.md","lastUpdated":1745964106000}'),l={name:"notifications/Echobell.md"};function a(s,o,r,c,h,d){return n(),t("div",null,o[0]||(o[0]=[i("",5)]))}const f=e(l,[["render",a]]);export{u as __pageData,f as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as e,c as i,o as s,j as a,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_smtp.C9OYC6IZ.png",E=JSON.parse('{"title":"Email","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745241280000}'),r={name:"notifications/Email.md"};function l(m,t,c,d,p,f){return s(),i("div",null,t[0]||(t[0]=[a("h1",{id:"email",tabindex:"-1"},[o("Email "),a("a",{class:"header-anchor",href:"#email","aria-label":'Permalink to "Email"'},"")],-1),a("p",null,[a("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",l]]);export{E as __pageData,u as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Email.md.n24Ra-lu.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as i,o as t,ag as r}from"./chunks/framework.DPDPlp3K.js";const f=JSON.parse('{"title":"Email Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745963141000}'),s={name:"notifications/Email.md"};function a(n,e,l,c,d,m){return t(),i("div",null,e[0]||(e[0]=[r('<h1 id="email-notification-setup" tabindex="-1">Email Notification Setup <a class="header-anchor" href="#email-notification-setup" aria-label="Permalink to "Email Notification Setup""></a></h1><p>To enable email or SMTP notifications, the following fields must be configured:</p><ul><li><p><strong>SMTP HOST</strong><br> The address of the SMTP server (e.g., <code>smtp.gmail.com</code> or <code>mail.example.com</code>).<br><em>→ Specifies which server will be used to send emails.</em></p></li><li><p><strong>SMTP PORT</strong><br> The port used for sending (typically <code>465</code> for SSL or <code>587</code> for TLS).<br><em>→ Defines the communication channel to the SMTP server.</em></p></li><li><p><strong>Secure Connection</strong><br> Indicates whether a secure connection is used (<code>SSL</code> or <code>TLS</code>).<br><em>→ Important for secure transmission of emails.</em></p></li><li><p><strong>SMTP Username</strong><br> The username for the email account (often the full email address).<br><em>→ Used to authenticate with the SMTP server.</em></p></li><li><p><strong>SMTP Password</strong><br> The corresponding password or an app-specific password.<br><em>→ Also required for authentication. Make sure to store it securely.</em></p></li><li><p><strong>From Address</strong><br> The sender's email address (e.g., <code>noreply@example.com</code>).<br><em>→ This address will appear as the sender in the recipient's inbox.</em></p></li><li><p><strong>To Address</strong><br> The recipient's email address where notifications should be sent.<br><em>→ Can be your personal email or a designated support inbox.</em></p></li></ul>',3)]))}const u=o(s,[["render",a]]);export{f as __pageData,u as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Email.md.n24Ra-lu.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as i,o as t,ag as r}from"./chunks/framework.DPDPlp3K.js";const f=JSON.parse('{"title":"Email Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Email.md","filePath":"notifications/Email.md","lastUpdated":1745963141000}'),s={name:"notifications/Email.md"};function a(n,e,l,c,d,m){return t(),i("div",null,e[0]||(e[0]=[r("",3)]))}const u=o(s,[["render",a]]);export{f as __pageData,u as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Gotify.md.D36rLkt7.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as e,o as i,ag as a}from"./chunks/framework.DPDPlp3K.js";const g=JSON.parse('{"title":"Gotify Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745963303000}'),n={name:"notifications/Gotify.md"};function s(r,t,l,f,c,p){return i(),e("div",null,t[0]||(t[0]=[a('<h1 id="gotify-notification-setup" tabindex="-1">Gotify Notification Setup <a class="header-anchor" href="#gotify-notification-setup" aria-label="Permalink to "Gotify Notification Setup""></a></h1><p>To enable Gotify notifications, you need the following information from your Gotify server:</p><ul><li><p><strong>Gotify URL</strong><br> The base URL of your Gotify server (e.g., <code>https://gotify.example.com</code>).</p></li><li><p><strong>Gotify Token</strong><br> The application token used to authenticate and send messages.</p></li></ul><h2 id="how-to-get-these-values" tabindex="-1">How to get these values: <a class="header-anchor" href="#how-to-get-these-values" aria-label="Permalink to "How to get these values:""></a></h2><ol><li><strong>Log in to your Gotify server.</strong></li><li><strong>Go to the "Applications" section.</strong></li><li><strong>Create a new application</strong> (e.g., "System Alerts").</li><li><strong>Copy the generated token</strong> — this is your Gotify Token.</li><li><strong>Use your server's URL</strong> as the Gotify URL.</li></ol>',5)]))}const h=o(n,[["render",s]]);export{g as __pageData,h as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Gotify.md.D36rLkt7.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as e,o as i,ag as a}from"./chunks/framework.DPDPlp3K.js";const g=JSON.parse('{"title":"Gotify Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745963303000}'),n={name:"notifications/Gotify.md"};function s(r,t,l,f,c,p){return i(),e("div",null,t[0]||(t[0]=[a("",5)]))}const h=o(n,[["render",s]]);export{g as __pageData,h as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as e,c as o,o as i,j as t,a as s}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_gotify.DDAcVx4N.png",y=JSON.parse('{"title":"Gotify","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Gotify.md","filePath":"notifications/Gotify.md","lastUpdated":1745241280000}'),r={name:"notifications/Gotify.md"};function f(c,a,d,l,p,m){return i(),o("div",null,a[0]||(a[0]=[t("h1",{id:"gotify",tabindex:"-1"},[s("Gotify "),t("a",{class:"header-anchor",href:"#gotify","aria-label":'Permalink to "Gotify"'},"")],-1),t("p",null,[t("img",{src:n,alt:"Set up"})],-1)]))}const u=e(r,[["render",f]]);export{y as __pageData,u as default};
|
||||
5
docs/.vitepress/dist/assets/notifications_Ntfy.md.BPwrZ9j5.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import{_ as i,c as t,o as a,ag as e}from"./chunks/framework.DPDPlp3K.js";const g=JSON.parse('{"title":"ntfy Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745963518000}'),n={name:"notifications/Ntfy.md"};function l(h,s,o,p,k,r){return a(),t("div",null,s[0]||(s[0]=[e(`<h1 id="ntfy-notification-setup" tabindex="-1">ntfy Notification Setup <a class="header-anchor" href="#ntfy-notification-setup" aria-label="Permalink to "ntfy Notification Setup""></a></h1><p>To enable ntfy notifications, you need the following:</p><ul><li><p><strong>ntfy URL</strong><br> The base URL of your ntfy server including the topic (e.g., <code>https://ntfy.example.com/alerts</code>)</p></li><li><p><strong>ntfy Token</strong><br> An access token for authentication, generated per user</p></li></ul><h2 id="how-to-get-the-ntfy-url-and-token" tabindex="-1">How to get the ntfy URL and Token <a class="header-anchor" href="#how-to-get-the-ntfy-url-and-token" aria-label="Permalink to "How to get the ntfy URL and Token""></a></h2><ol><li><p><strong>Install and set up your ntfy server</strong> (self-hosted or use <code>https://ntfy.sh</code>)</p></li><li><p><strong>Choose a topic name</strong> (e.g. <code>alerts</code>) and include it in the URL:<br><code>https://<your-ntfy-server>/<your-topic></code></p></li><li><p><strong>Create a user (if not already created)</strong></p></li><li><p><strong>Generate a token for the user</strong> using the following command:</p><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;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --expires=30d</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --label=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"notifications"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">usernam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">e</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">></span></span></code></pre></div></li><li><p><strong>List existing tokens</strong> to get the full token string:</p><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;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> list</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">usernam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">e</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">></span></span></code></pre></div></li><li><p><strong>Use the token</strong> as a bearer token when sending messages, either in the Authorization header or in your tool's configuration.</p></li></ol><h2 id="example-token-management-commands" tabindex="-1">Example Token Management Commands <a class="header-anchor" href="#example-token-management-commands" aria-label="Permalink to "Example Token Management Commands""></a></h2><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;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> list</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Show all tokens</span></span>
|
||||
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> list</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> alice</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Show tokens for user 'alice'</span></span>
|
||||
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> alice</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Create token for user 'alice' (never expires)</span></span>
|
||||
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --expires=2d</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> bob</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Create token for 'bob', expires in 2 days</span></span>
|
||||
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ntfy</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> token</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> remove</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> alice</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> tk_...</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Delete a token</span></span></code></pre></div><p>More information at <a href="https://docs.ntfy.sh/config/#access-tokens" target="_blank" rel="noreferrer">the ntfy docs</a></p>`,8)]))}const c=i(n,[["render",l]]);export{g as __pageData,c as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Ntfy.md.BPwrZ9j5.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as i,c as t,o as a,ag as e}from"./chunks/framework.DPDPlp3K.js";const g=JSON.parse('{"title":"ntfy Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745963518000}'),n={name:"notifications/Ntfy.md"};function l(h,s,o,p,k,r){return a(),t("div",null,s[0]||(s[0]=[e("",8)]))}const c=i(n,[["render",l]]);export{g as __pageData,c as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as a,c as n,o as s,j as t,a as o}from"./chunks/framework.DPDPlp3K.js";const i="/assets/notifications_ntfy.OOek8qxp.png",y=JSON.parse('{"title":"Ntfy","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Ntfy.md","filePath":"notifications/Ntfy.md","lastUpdated":1745241280000}'),r={name:"notifications/Ntfy.md"};function f(c,e,d,l,p,m){return s(),n("div",null,e[0]||(e[0]=[t("h1",{id:"ntfy",tabindex:"-1"},[o("Ntfy "),t("a",{class:"header-anchor",href:"#ntfy","aria-label":'Permalink to "Ntfy"'},"")],-1),t("p",null,[t("img",{src:i,alt:"Set up"})],-1)]))}const N=a(r,[["render",f]]);export{y as __pageData,N as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Pushover.md.B37wP4uj.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as t,o as r,ag as n}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Pushover Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745963844000}'),s={name:"notifications/Pushover.md"};function i(a,e,u,l,h,p){return r(),t("div",null,e[0]||(e[0]=[n('<h1 id="pushover-notification-setup" tabindex="-1">Pushover Notification Setup <a class="header-anchor" href="#pushover-notification-setup" aria-label="Permalink to "Pushover Notification Setup""></a></h1><p>To enable Pushover notifications, you need the following:</p><ul><li><p><strong>Pushover URL</strong><br> The API endpoint for sending messages:<br><code>https://api.pushover.net/1/messages.json</code></p></li><li><p><strong>Pushover Token</strong><br> Your application’s API token (generated in your Pushover dashboard)</p></li><li><p><strong>Pushover User Key</strong><br> The user key or group key of the recipient (found in your Pushover account)</p></li></ul><h2 id="how-to-get-the-url-token-and-user-key" tabindex="-1">How to get the URL, Token, and User Key <a class="header-anchor" href="#how-to-get-the-url-token-and-user-key" aria-label="Permalink to "How to get the URL, Token, and User Key""></a></h2><ol><li><strong>Sign up or log in</strong> at the Pushover website.</li><li><strong>Create a new application</strong> under “Your Applications.” <ul><li>You will receive your <strong>Pushover Token</strong> here.</li></ul></li><li><strong>Locate your User Key</strong> on your account’s main page. <ul><li>If you want to notify a group, create or use an existing <strong>Group Key</strong> instead.</li></ul></li><li><strong>Use the API URL</strong> <code>https://api.pushover.net/1/messages.json</code></li></ol>',5)]))}const g=o(s,[["render",i]]);export{d as __pageData,g as default};
|
||||
1
docs/.vitepress/dist/assets/notifications_Pushover.md.B37wP4uj.lean.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import{_ as o,c as t,o as r,ag as n}from"./chunks/framework.DPDPlp3K.js";const d=JSON.parse('{"title":"Pushover Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745963844000}'),s={name:"notifications/Pushover.md"};function i(a,e,u,l,h,p){return r(),t("div",null,e[0]||(e[0]=[n("",5)]))}const g=o(s,[["render",i]]);export{d as __pageData,g as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as s,c as a,o,j as e,a as r}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_pushover.CeUzFKPr.png",m=JSON.parse('{"title":"Pushover","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745496781000}'),i={name:"notifications/Pushover.md"};function c(p,t,d,l,u,h){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"pushover",tabindex:"-1"},[r("Pushover "),e("a",{class:"header-anchor",href:"#pushover","aria-label":'Permalink to "Pushover"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Set up"})],-1)]))}const v=s(i,[["render",c]]);export{m as __pageData,v as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as s,c as a,o,j as e,a as r}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_pushover.CeUzFKPr.png",m=JSON.parse('{"title":"Pushover","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Pushover.md","filePath":"notifications/Pushover.md","lastUpdated":1745496781000}'),i={name:"notifications/Pushover.md"};function c(p,t,d,l,u,h){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"pushover",tabindex:"-1"},[r("Pushover "),e("a",{class:"header-anchor",href:"#pushover","aria-label":'Permalink to "Pushover"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Set up"})],-1)]))}const v=s(i,[["render",c]]);export{m as __pageData,v as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};
|
||||
@ -1 +0,0 @@
|
||||
import{_ as t,c as r,o as s,j as e,a as o}from"./chunks/framework.DPDPlp3K.js";const n="/assets/notifications_telegram.CETmcOHu.png",_=JSON.parse('{"title":"Telegram","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745241280000}'),i={name:"notifications/Telegram.md"};function l(m,a,c,d,p,g){return s(),r("div",null,a[0]||(a[0]=[e("h1",{id:"telegram",tabindex:"-1"},[o("Telegram "),e("a",{class:"header-anchor",href:"#telegram","aria-label":'Permalink to "Telegram"'},"")],-1),e("p",null,[e("img",{src:n,alt:"Telegram"})],-1)]))}const T=t(i,[["render",l]]);export{_ as __pageData,T as default};
|
||||
16
docs/.vitepress/dist/assets/notifications_Telegram.md.B9HZvnCz.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
import{_ as e,c as n,o as l,ag as t,j as s,a as i}from"./chunks/framework.DPDPlp3K.js";const c=JSON.parse('{"title":"Telegram Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745963966000}'),h={name:"notifications/Telegram.md"};function p(o,a,k,r,d,E){return l(),n("div",null,a[0]||(a[0]=[t('<h1 id="telegram-notification-setup" tabindex="-1">Telegram Notification Setup <a class="header-anchor" href="#telegram-notification-setup" aria-label="Permalink to "Telegram Notification Setup""></a></h1><p>To enable Telegram notifications, you need the following:</p><ul><li><p><strong>Bot Token</strong><br> Generated by @BotFather when you create your bot.</p></li><li><p><strong>Chat ID</strong><br> A unique identifier for the target chat (user, group, or channel).</p></li></ul><h2 id="how-to-create-the-bot-and-get-the-bot-token" tabindex="-1">How to create the bot and get the Bot Token <a class="header-anchor" href="#how-to-create-the-bot-and-get-the-bot-token" aria-label="Permalink to "How to create the bot and get the Bot Token""></a></h2>',4),s("ol",null,[s("li",null,[i("Open Telegram and start a conversation with "),s("strong",null,"@BotFather"),i(".")]),s("li",{index:"1"},[i("Send the command "),s("code",null,"/newbot"),i(", then follow the prompts to choose a name and username (must end with “bot”). :contentReference[oaicite:1]")]),s("li",null,[i("After completion, @BotFather replies with a message containing:"),s("div",{class:"language- vp-adaptive-theme"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"}),s("pre",{class:"shiki shiki-themes github-light github-dark vp-code",tabindex:"0"},[s("code",null,[s("span",{class:"line"},[s("span",null,"Use this token to access the HTTP API:")]),i(`
|
||||
`),s("span",{class:"line"},[s("span",null,"123456789:ABCdefGhIJKlmNoPQRsTuvWxYZ")])])])]),i("Copy this token—this is your "),s("strong",null,"Bot Token"),i(".")])],-1),s("h2",{id:"how-to-obtain-the-chat-id",tabindex:"-1"},[i("How to obtain the Chat ID "),s("a",{class:"header-anchor",href:"#how-to-obtain-the-chat-id","aria-label":'Permalink to "How to obtain the Chat ID"'},"")],-1),s("ol",null,[s("li",null,"Start a chat with your new bot (send it any message)."),s("li",null,[i("Open in your browser:"),s("div",{class:"language- vp-adaptive-theme"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"}),s("pre",{class:"shiki shiki-themes github-light github-dark vp-code",tabindex:"0"},[s("code",null,[s("span",{class:"line"},[s("span",null,"https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates")])])])])]),s("li",{index:"3"},[i("Look for the "),s("code",null,'"chat":{"id":...}'),i(" field in the returned JSON. That number is the "),s("strong",null,"Chat ID"),i(". :contentReference[oaicite:3]")])],-1),t(`<h3 id="example-getupdates-response-excerpt" tabindex="-1">Example: getUpdates response excerpt <a class="header-anchor" href="#example-getupdates-response-excerpt" aria-label="Permalink to "Example: getUpdates response excerpt""></a></h3><div class="language-json vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">{</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "ok"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "result"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: [</span></span>
|
||||
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "update_id"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">123456789</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "message"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: {</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "message_id"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "from"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"id"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">987654321</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"is_bot"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"first_name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"User"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "chat"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"id"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">987654321</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"first_name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"User"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">"type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"private"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "date"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1610000000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
||||
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> "text"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"Hello"</span></span>
|
||||
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
|
||||
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
|
||||
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> ]</span></span>
|
||||
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p><em>Here, the Chat ID is <code>987654321</code>.</em></p>`,3)]))}const u=e(h,[["render",p]]);export{c as __pageData,u as default};
|
||||
2
docs/.vitepress/dist/assets/notifications_Telegram.md.B9HZvnCz.lean.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import{_ as e,c as n,o as l,ag as t,j as s,a as i}from"./chunks/framework.DPDPlp3K.js";const c=JSON.parse('{"title":"Telegram Notification Setup","description":"","frontmatter":{},"headers":[],"relativePath":"notifications/Telegram.md","filePath":"notifications/Telegram.md","lastUpdated":1745963966000}'),h={name:"notifications/Telegram.md"};function p(o,a,k,r,d,E){return l(),n("div",null,a[0]||(a[0]=[t("",4),s("ol",null,[s("li",null,[i("Open Telegram and start a conversation with "),s("strong",null,"@BotFather"),i(".")]),s("li",{index:"1"},[i("Send the command "),s("code",null,"/newbot"),i(", then follow the prompts to choose a name and username (must end with “bot”). :contentReference[oaicite:1]")]),s("li",null,[i("After completion, @BotFather replies with a message containing:"),s("div",{class:"language- vp-adaptive-theme"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"}),s("pre",{class:"shiki shiki-themes github-light github-dark vp-code",tabindex:"0"},[s("code",null,[s("span",{class:"line"},[s("span",null,"Use this token to access the HTTP API:")]),i(`
|
||||
`),s("span",{class:"line"},[s("span",null,"123456789:ABCdefGhIJKlmNoPQRsTuvWxYZ")])])])]),i("Copy this token—this is your "),s("strong",null,"Bot Token"),i(".")])],-1),s("h2",{id:"how-to-obtain-the-chat-id",tabindex:"-1"},[i("How to obtain the Chat ID "),s("a",{class:"header-anchor",href:"#how-to-obtain-the-chat-id","aria-label":'Permalink to "How to obtain the Chat ID"'},"")],-1),s("ol",null,[s("li",null,"Start a chat with your new bot (send it any message)."),s("li",null,[i("Open in your browser:"),s("div",{class:"language- vp-adaptive-theme"},[s("button",{title:"Copy Code",class:"copy"}),s("span",{class:"lang"}),s("pre",{class:"shiki shiki-themes github-light github-dark vp-code",tabindex:"0"},[s("code",null,[s("span",{class:"line"},[s("span",null,"https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates")])])])])]),s("li",{index:"3"},[i("Look for the "),s("code",null,'"chat":{"id":...}'),i(" field in the returned JSON. That number is the "),s("strong",null,"Chat ID"),i(". :contentReference[oaicite:3]")])],-1),t("",3)]))}const u=e(h,[["render",p]]);export{c as __pageData,u as default};
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
docs/.vitepress/dist/assets/settings_language.CCbF4jzs.png
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
8
docs/.vitepress/dist/general/Dashboard.html
vendored
8
docs/.vitepress/dist/general/Network.html
vendored
8
docs/.vitepress/dist/general/Servers.html
vendored
10
docs/.vitepress/dist/general/Settings.html
vendored
8
docs/.vitepress/dist/general/Uptime.html
vendored
2
docs/.vitepress/dist/hashmap.json
vendored
@ -1 +1 @@
|
||||
{"general_applications.md":"DFVqSlCw","general_dashboard.md":"DW5yESFW","general_network.md":"tbP8aEzX","general_servers.md":"BaASA60T","general_settings.md":"DrC2XV32","general_uptime.md":"CKBdQg4u","index.md":"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"}
|
||||
{"general_applications.md":"DFVqSlCw","general_dashboard.md":"DW5yESFW","general_network.md":"tbP8aEzX","general_servers.md":"BaASA60T","general_settings.md":"DG8ZT4OR","general_uptime.md":"CKBdQg4u","index.md":"vIfS0_LS","installation.md":"RudnHaMh","notifications_discord.md":"D5alp298","notifications_echobell.md":"IszWXk9P","notifications_email.md":"n24Ra-lu","notifications_general.md":"D7AVsSjD","notifications_gotify.md":"D36rLkt7","notifications_ntfy.md":"BPwrZ9j5","notifications_pushover.md":"B37wP4uj","notifications_telegram.md":"B9HZvnCz"}
|
||||
|
||||
8
docs/.vitepress/dist/index.html
vendored
@ -8,12 +8,12 @@
|
||||
<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>
|
||||
<script type="module" src="/assets/chunks/metadata.80a3dac1.js"></script>
|
||||
<script type="module" src="/assets/app.C_TDNGCa.js"></script>
|
||||
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.9-rJywIy.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/theme.BsFEzhuB.js">
|
||||
<link rel="modulepreload" href="/assets/chunks/framework.DPDPlp3K.js">
|
||||
<link rel="modulepreload" href="/assets/index.md.BeIP42w_.lean.js">
|
||||
<link rel="modulepreload" href="/assets/index.md.vIfS0_LS.lean.js">
|
||||
<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>
|
||||
|
||||
12
docs/.vitepress/dist/installation.html
vendored
12
docs/.vitepress/dist/notifications/Discord.html
vendored
26
docs/.vitepress/dist/notifications/Echobell.html
vendored
Normal file
12
docs/.vitepress/dist/notifications/Email.html
vendored
12
docs/.vitepress/dist/notifications/Gotify.html
vendored
16
docs/.vitepress/dist/notifications/Ntfy.html
vendored
12
docs/.vitepress/dist/notifications/Pushover.html
vendored
27
docs/.vitepress/dist/notifications/Telegram.html
vendored
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
docs/assets/screenshots/settings_language.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
@ -4,8 +4,19 @@ Here you can manage the complete settings of CoreControl.
|
||||
## User Settings
|
||||

|
||||
|
||||
You can change your email and password in the user settings. Please note that you need your old password to change your password.
|
||||
|
||||
## Theme Settings
|
||||

|
||||
|
||||
With the theme settings you have the choice between light and dark mode. There is also the option to select “System”, where the system settings are applied.
|
||||
|
||||
## Language Settings
|
||||

|
||||
|
||||
To promote internationalization (also often known as i18n), you can select the language in which you want everything to be displayed within CoreControl. Currently there is the standard language “English” and the language German.
|
||||
|
||||
## Notification Settings
|
||||

|
||||
|
||||
To receive notifications from CoreControl, you can add all your notification providers here. You can also customize the notification text.
|
||||
@ -4,10 +4,6 @@ The easiest way to install CoreControl is using Docker Compose. Follow these ste
|
||||
|
||||
## Docker Compose Installation
|
||||
|
||||
::: danger
|
||||
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.
|
||||
:::
|
||||
|
||||
1. Make sure [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your system.
|
||||
|
||||
2. Create a file named `docker-compose.yml` with the following content:
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
# Discord
|
||||
# Discord Notification Setup
|
||||
|
||||

|
||||
To enable Discord notifications, you will need a **Discord Webhook URL**.
|
||||
This URL allows the system to send messages directly to a specific Discord channel.
|
||||
|
||||
You can create a webhook by following this [official Discord guide](https://support.discord.com/hc/articles/228383668).
|
||||
Once created, simply paste the webhook URL into the designated field in your notification settings.
|
||||
17
docs/notifications/Echobell.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Echobell Notification Setup
|
||||
|
||||
To enable Echobell notifications, you need the following:
|
||||
|
||||
- **Echobell Webhook URL**
|
||||
The HTTP POST endpoint that Echobell exposes for your channel. You’ll find it in your channel’s **Integrations → Webhooks** section.
|
||||
|
||||
- **Message Field Key**
|
||||
The JSON field name that Echobell expects for the notification text. By default this is `message`, but you can verify or customize it under **Integrations → Webhooks → Payload Settings**.
|
||||
|
||||
## How to get your Webhook URL and field key
|
||||
|
||||
1. **Log in** to your Echobell account.
|
||||
2. **Select the channel** you want to send notifications to.
|
||||
3. Navigate to **Integrations → Webhooks**.
|
||||
- Copy the **Webhook URL** shown there (e.g., `https://api.echobell.one/hooks/abc123`).
|
||||
4. In the same screen, check **Payload Settings** and confirm the **Field Key** for your message (`message`).
|
||||
@ -1,3 +1,31 @@
|
||||
# Email
|
||||
# Email Notification Setup
|
||||
|
||||

|
||||
To enable email or SMTP notifications, the following fields must be configured:
|
||||
|
||||
- **SMTP HOST**
|
||||
The address of the SMTP server (e.g., `smtp.gmail.com` or `mail.example.com`).
|
||||
*→ Specifies which server will be used to send emails.*
|
||||
|
||||
- **SMTP PORT**
|
||||
The port used for sending (typically `465` for SSL or `587` for TLS).
|
||||
*→ Defines the communication channel to the SMTP server.*
|
||||
|
||||
- **Secure Connection**
|
||||
Indicates whether a secure connection is used (`SSL` or `TLS`).
|
||||
*→ Important for secure transmission of emails.*
|
||||
|
||||
- **SMTP Username**
|
||||
The username for the email account (often the full email address).
|
||||
*→ Used to authenticate with the SMTP server.*
|
||||
|
||||
- **SMTP Password**
|
||||
The corresponding password or an app-specific password.
|
||||
*→ Also required for authentication. Make sure to store it securely.*
|
||||
|
||||
- **From Address**
|
||||
The sender's email address (e.g., `noreply@example.com`).
|
||||
*→ This address will appear as the sender in the recipient's inbox.*
|
||||
|
||||
- **To Address**
|
||||
The recipient's email address where notifications should be sent.
|
||||
*→ Can be your personal email or a designated support inbox.*
|
||||
|
||||
@ -1,3 +1,17 @@
|
||||
# Gotify
|
||||
# Gotify Notification Setup
|
||||
|
||||

|
||||
To enable Gotify notifications, you need the following information from your Gotify server:
|
||||
|
||||
- **Gotify URL**
|
||||
The base URL of your Gotify server (e.g., `https://gotify.example.com`).
|
||||
|
||||
- **Gotify Token**
|
||||
The application token used to authenticate and send messages.
|
||||
|
||||
## How to get these values:
|
||||
|
||||
1. **Log in to your Gotify server.**
|
||||
2. **Go to the "Applications" section.**
|
||||
3. **Create a new application** (e.g., "System Alerts").
|
||||
4. **Copy the generated token** — this is your Gotify Token.
|
||||
5. **Use your server's URL** as the Gotify URL.
|
||||
@ -1,3 +1,41 @@
|
||||
# Ntfy
|
||||
# ntfy Notification Setup
|
||||
|
||||

|
||||
To enable ntfy notifications, you need the following:
|
||||
|
||||
- **ntfy URL**
|
||||
The base URL of your ntfy server including the topic (e.g., `https://ntfy.example.com/alerts`)
|
||||
|
||||
- **ntfy Token**
|
||||
An access token for authentication, generated per user
|
||||
|
||||
## How to get the ntfy URL and Token
|
||||
|
||||
1. **Install and set up your ntfy server** (self-hosted or use `https://ntfy.sh`)
|
||||
2. **Choose a topic name** (e.g. `alerts`) and include it in the URL:
|
||||
`https://<your-ntfy-server>/<your-topic>`
|
||||
|
||||
3. **Create a user (if not already created)**
|
||||
|
||||
4. **Generate a token for the user** using the following command:
|
||||
```bash
|
||||
ntfy token add --expires=30d --label="notifications" <username>
|
||||
```
|
||||
|
||||
5. **List existing tokens** to get the full token string:
|
||||
```bash
|
||||
ntfy token list <username>
|
||||
```
|
||||
|
||||
6. **Use the token** as a bearer token when sending messages, either in the Authorization header or in your tool's configuration.
|
||||
|
||||
## Example Token Management Commands
|
||||
|
||||
```bash
|
||||
ntfy token list # Show all tokens
|
||||
ntfy token list alice # Show tokens for user 'alice'
|
||||
ntfy token add alice # Create token for user 'alice' (never expires)
|
||||
ntfy token add --expires=2d bob # Create token for 'bob', expires in 2 days
|
||||
ntfy token remove alice tk_... # Delete a token
|
||||
```
|
||||
|
||||
More information at [the ntfy docs](https://docs.ntfy.sh/config/#access-tokens)
|
||||