109 Commits

Author SHA1 Message Date
headlesdev
94ba5e20c6 Revert "cleanup v2"
This reverts commit 8b82578809.
2025-05-17 12:31:27 +02:00
headlesdev
8b82578809 cleanup v2 2025-05-17 12:24:08 +02:00
headlessdev
64b5b5e1c4 Update README.md 2025-05-01 14:46:59 +02:00
headlessdev
04d55ac709 Update FUNDING.yml 2025-05-01 14:45:33 +02:00
headlessdev
135b3684a9 Bump version 2025-04-30 20:59:17 +02:00
headlessdev
c212986fb5 Bcrypt to Bcryptjs to support ARM based systems 2025-04-30 19:53:15 +02:00
headlessdev
dbf86ab283 Add binary targets to prisma generator 2025-04-30 19:32:35 +02:00
headlessdev
1ad84e7ea3 Docker startup command fix 2025-04-30 18:59:05 +02:00
headlessdev
1b75d95be4 Dockerfile Arch fix 2025-04-30 18:47:09 +02:00
headlessdev
634c7aaec3 Fix i18n Notifications Desciption 2025-04-30 18:40:45 +02:00
headlessdev
799a292471 Merge branch 'main' of https://github.com/crocofied/CoreControl 2025-04-30 18:35:23 +02:00
headlessdev
c624ddafb2 Updated Dockerfile to support ARM 2025-04-30 18:35:22 +02:00
headlessdev
ce8739181a Update README.md 2025-04-30 16:09:53 +02:00
headlessdev
cf177d587c Readme screenshot fix 2025-04-30 16:08:39 +02:00
headlessdev
1a36c591af v1.0.0->main
v1.0.0
2025-04-30 15:01:05 +02:00
headlessdev
fb5e145b8d Updated README images 2025-04-30 14:59:50 +02:00
headlessdev
e5bae66363 Dockerfile prisma fix 2025-04-30 14:57:19 +02:00
headlessdev
2188f76318 Hotify agent missing echobellUrl 2025-04-30 14:48:33 +02:00
headlessdev
912fd68a0f Dockerfile Web Fix 2025-04-30 14:41:11 +02:00
headlessdev
bc060664dd Servers mobile responsive 2025-04-30 00:26:33 +02:00
headlessdev
08f03a69a1 Uptime i18n IPP fix 2025-04-30 00:10:01 +02:00
headlessdev
ca4db2124b docs build 2025-04-30 00:02:59 +02:00
headlessdev
3325922ef6 Echobell docs 2025-04-30 00:01:46 +02:00
headlessdev
d574735caf Telegram docs 2025-04-29 23:59:26 +02:00
headlessdev
c853da9d50 Pushover docs 2025-04-29 23:57:24 +02:00
headlessdev
ee040c9f7f Fix Pushover Settings i18n 2025-04-29 23:53:27 +02:00
headlessdev
509e83eb7c Ntfy docs 2025-04-29 23:51:58 +02:00
headlessdev
78118c5985 Gotify docs headers fix 2025-04-29 23:48:23 +02:00
headlessdev
2203ee2f01 Gotify Docs 2025-04-29 23:48:04 +02:00
headlessdev
65ec1dc827 Dsicord & Email Docs 2025-04-29 23:45:41 +02:00
headlessdev
5414045240 SMTP Notification Settings i18n fix 2025-04-29 23:42:37 +02:00
headlessdev
e8bd50ca20 Discord Docs 2025-04-29 23:41:14 +02:00
headlessdev
fee6b9c3dd Updated Settings Docs Page 2025-04-29 23:35:18 +02:00
headlessdev
eb55772560 bump docs version 2025-04-29 23:22:07 +02:00
headlessdev
10ad1e24fa Remopve Beta notice from docs 2025-04-29 23:21:39 +02:00
headlessdev
fde06f0bcc Fix generate app icon url 2025-04-29 23:05:53 +02:00
headlessdev
03e9439d7d Echobell notice i18n 2025-04-29 22:54:32 +02:00
headlessdev
1fccc59c01 Add Echobell provider 2025-04-29 22:52:29 +02:00
headlessdev
1a395783b0 Add Echobell notification fields and i18n support in settings 2025-04-29 22:40:07 +02:00
headlessdev
0f2afb4157 Add echobell fields to notification request handling 2025-04-29 22:28:42 +02:00
headlessdev
073cd00120 add echobell to notifications model 2025-04-29 22:20:18 +02:00
headlessdev
91e7e47d9b Update Server.tsx to enhance i18n labels for temperature and usage history 2025-04-29 22:06:22 +02:00
headlessdev
e069246e18 Update FUNDING.yml 2025-04-29 21:28:50 +02:00
headlessdev
76e8ee837f Update FUNDING.yml 2025-04-29 21:26:08 +02:00
headlessdev
c3536ce715 Updated Dockerfiles for ARM Support 2025-04-29 21:17:21 +02:00
headlessdev
176dfac0c1 Refactor notification handling to track server status changes instead of notification timestamps 2025-04-29 20:40:56 +02:00
headlessdev
93dccaf0d5 Fix icon Language Setting 2025-04-29 20:27:21 +02:00
headlessdev
ead8bd7408 Fix server notifications been sent multiple times on status change in agent 2025-04-29 20:25:57 +02:00
headlessdev
502f749151 Language selection 2025-04-29 20:14:44 +02:00
headlessdev
5adb7b3967 Server.tsx i18n fix 2025-04-29 19:43:01 +02:00
headlessdev
a2df2f2a29 Application.Card.Server de 2025-04-29 19:39:54 +02:00
headlessdev
3e4f7c3641 i18n final 2025-04-29 19:38:48 +02:00
headlessdev
58dd396241 Server.tsx i18n 2025-04-29 16:33:34 +02:00
headlessdev
8614ec12f3 Merge branch 'v1.0.0' of https://github.com/crocofied/CoreControl into v1.0.0 2025-04-29 16:20:35 +02:00
headlessdev
c4f3b47fc7 Servers.tsx i18n 2025-04-29 16:20:34 +02:00
headlessdev
196b70b2ad Delete crowdin.yml 2025-04-29 00:03:53 +02:00
headlessdev
faea966f2f Update Crowdin configuration file 2025-04-29 00:00:32 +02:00
headlessdev
58e2466875 Servers.tsx i18n 1/2 2025-04-28 23:50:18 +02:00
headlessdev
eff7901b67 Sidebar i18n 2025-04-28 23:28:34 +02:00
headlessdev
4665b22514 Dashboard i18n 2025-04-28 23:25:32 +02:00
headlessdev
ff49825eab i18n implementation & Login i18n 2025-04-28 23:19:55 +02:00
headlessdev
58bd039635 Applications.tsx mobile friendly 2025-04-28 22:27:39 +02:00
headlessdev
34493d783f Serevrs.txt mobile friendly 2025-04-28 22:23:50 +02:00
headlessdev
5ba95c4b1a Add function if a server has no temp or gpu 2025-04-28 21:50:21 +02:00
headlessdev
df2769a322 Tempreture & GPU View in Server Component 2025-04-28 20:30:51 +02:00
headlessdev
9a35a6ca50 Add temp & gpu display in servers overview 2025-04-28 20:28:52 +02:00
headlessdev
c9f1d32038 Update API Routes to include Temp & GPU Data 2025-04-28 20:12:46 +02:00
headlessdev
934768fec8 GPU & Temp receiving in agent 2025-04-28 20:01:51 +02:00
headlessdev
7fc3703a05 Add gpu & temp models agent 2025-04-28 19:53:04 +02:00
headlessdev
61bf108f40 Bump version 2025-04-28 19:48:59 +02:00
headlessdev
07497b1832 add gpuUsage and temp fields to server and server_history models in schema.prisma 2025-04-28 19:48:30 +02:00
headlessdev
245ffdd3dd Merge pull request #58 from crocofied/issue_templates
.github (Issue Templates & more)
2025-04-28 15:57:04 +02:00
headlessdev
f9617794f9 Add code of conduct 2025-04-28 15:56:06 +02:00
headlessdev
0ab788130b Add Contributing & Funding 2025-04-28 15:55:20 +02:00
headlessdev
af7562f750 Add PULL_REQUEST_TEMPLATE 2025-04-28 15:53:27 +02:00
headlessdev
9974969ed9 Add github issues templates 2025-04-28 15:52:11 +02:00
headlessdev
a710596425 Update README.md 2025-04-28 11:36:07 +02:00
headlessdev
f7697ff925 Merge pull request #56 from pixitha/main
Update app-sidebar.tsx to open github in new page
2025-04-28 11:34:51 +02:00
Kyle Duren
e1eff2baf8 Update app-sidebar.tsx to open github in new page
Made the github link open in a new page, and also not send the referrer, since this is probably running on an internal system, we don't need to send github all our internal hostname info.
2025-04-27 20:29:09 -04:00
headlessdev
d63343af7a Fix agent uptimecheckUrl 2025-04-27 22:02:21 +02:00
headlessdev
5e0111032d Fix agent uptimecheckurl 2025-04-27 22:00:09 +02:00
headlessdev
c34ec11073 Remove error file 2025-04-27 21:19:45 +02:00
headlessdev
d7e7ac8322 Fixed version 2025-04-27 21:17:22 +02:00
headlessdev
6acaf82974 migrations 2025-04-27 16:26:18 +02:00
headlessdev
fa03cc154f Delete prisma/migrations 2025-04-27 16:20:17 +02:00
headlessdev
4a262f7563 Create migrations 2025-04-27 16:20:10 +02:00
headlessdev
799d71cdae Delete prisma/migrations directory 2025-04-27 16:19:31 +02:00
headlessdev
51bd61622e v0.0.10 -> main
V0.0.10
2025-04-27 16:03:02 +02:00
headlessdev
127077d929 Added two more icons for server icons 2025-04-27 15:33:34 +02:00
headlessdev
fcb7196d1c Enhance application monitoring by integrating custom uptime check URL support. Updated database queries and application model to include UptimeCheckURL, allowing for more flexible monitoring options. 2025-04-27 15:28:00 +02:00
headlessdev
30aa4bcf57 Add custom uptime check feature to Applications dashboard, allowing users to specify a custom URL for uptime monitoring. Updated state management and UI components to support this functionality. 2025-04-27 15:23:37 +02:00
headlessdev
3dc2c6b204 Add uptimecheckUrl to AddRequest and EditRequest interfaces, updating application creation and editing logic accordingly 2025-04-27 14:58:44 +02:00
headlessdev
65f8e16fbc Notification Name 2025-04-27 14:56:40 +02:00
headlessdev
807d4d6ca9 Add name field to AddRequest interface and update notification creation logic 2025-04-27 14:49:36 +02:00
headlessdev
70e28cb1c7 Add uptimecheckUrl to application model and name to notification model 2025-04-27 14:45:09 +02:00
headlessdev
cba70d5aef Updated Agent Dockerfile 2025-04-26 16:13:04 +02:00
headlessdev
78c844b1fe Split go agent into multiple files for better overview 2025-04-26 16:11:06 +02:00
headlessdev
00a628031e Finish úptime display 2025-04-26 15:38:39 +02:00
headlessdev
a84a985645 Display Uptime in Servers Page 2025-04-26 15:21:20 +02:00
headlessdev
b08d5b0fef Add uptime column to server model 2025-04-26 15:13:00 +02:00
headlessdev
58f70ad1d9 Application Grid View UI Bug Fix 2025-04-26 15:06:46 +02:00
headlessdev
2f61974f6e New view selection menu in servers view 2025-04-26 13:18:26 +02:00
headlessdev
fde7b3b23e New compact view for applications 2025-04-26 13:11:19 +02:00
headlessdev
677d1c5a58 Add custom items per page dropdowns 2025-04-25 23:34:52 +02:00
headlessdev
d81d8c04ad Fixed missing space in docs 2025-04-25 22:57:48 +02:00
headlessdev
b368075b12 Fix ntfy notifications 2025-04-25 22:51:34 +02:00
headlessdev
7ad4390db7 Fix SMTP Notifications 2025-04-25 22:34:20 +02:00
headlessdev
408b80badf Fix allocated server not showing in applications search result 2025-04-25 22:13:24 +02:00
headlessdev
8223cc5822 Memory not displayed fix 2025-04-25 22:11:39 +02:00
163 changed files with 4817 additions and 1860 deletions

43
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery, and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project leaders are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [your email address or contact method here]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), available at [https://www.contributor-covenant.org/](https://www.contributor-covenant.org/).

8
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,8 @@
# How to Contribute
1. Fork the repository
2. Create a new branch
3. Make your changes
4. Submit a pull request
Please make sure your code passes all tests and follows the style guide.

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: crocofied
buy_me_a_coffee: corecontrol

67
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: 🐞 - Bug Report
description: Report a problem or unexpected behavior.
title: "[Bug]: "
labels: ["unverified bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
Thank you for reporting a bug! Please fill out the information below.
- type: input
id: what-happened
attributes:
label: What happened?
description: Describe the issue you encountered.
placeholder: Tell us what happened...
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to reproduce
description: List the steps needed to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: dropdown
id: operating-system
attributes:
label: Operating System
options:
- Windows
- macOS
- Linux
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The version of the CoreControl you are using.
placeholder: e.g. v0.0.10
validations:
required: true
- type: input
id: logs
attributes:
label: Logs
description: The logs of the docker containers, if necessary.
placeholder: |
```
[logs]
```
validations:
required: false
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Add any other context about the problem here.
placeholder: Any extra details...

View File

@@ -0,0 +1,41 @@
name: ✨ - Feature Request
description: Suggest a new idea or enhancement.
title: "[Feature]: "
labels: ["enhancement"]
assignees: []
body:
- type: markdown
attributes:
value: |
Thank you for suggesting a feature! Please provide as much detail as possible.
- type: input
id: feature-summary
attributes:
label: Feature Summary
description: Short summary of the feature you are requesting.
placeholder: A short and clear description...
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
description: Why do you need this feature? What problem does it solve?
placeholder: Describe the use case...
validations:
required: true
- type: textarea
id: possible-solution
attributes:
label: Possible Solution
description: Suggest an idea for how the feature could be implemented (optional).
placeholder: Maybe something like this...
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context, screenshots, or examples here.
placeholder: Other relevant information...

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,7 @@
## Description
Please explain the changes you made.
## Checklist
- [ ] I have tested the code
- [ ] I have updated the documentation if necessary
- [ ] I followed the project's coding guidelines

1
.github/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,47 +1,53 @@
# Builder Stage # Builder Stage
FROM node:20-alpine AS builder FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
ARG TARGETARCH # Automatically set by Buildx
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
COPY ./prisma ./prisma COPY ./prisma ./prisma
# Install all dependencies (including devDependencies) # Set PRISMA_CLI_BINARY_TARGETS based on TARGETARCH and install dependencies
RUN npm install RUN case ${TARGETARCH} in \
"amd64") export PRISMA_CLI_BINARY_TARGETS="linux-musl-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 && \
npm install && \
npx prisma generate
# Generate Prisma client
RUN npx prisma generate
# Build the application
COPY . . COPY . .
RUN npm run build RUN npm run build
# Production Stage # Production Stage
FROM node:20-alpine AS production FROM --platform=$TARGETPLATFORM node:20-alpine AS production
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
# Copy package files # Copy built assets and dependencies from builder
COPY package.json package-lock.json* ./
# Copy node_modules from builder
COPY --from=builder /app/node_modules ./node_modules 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 --from=builder /app/prisma ./prisma
# Copy built application
COPY --from=builder /app/.next ./.next COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/next.config.js* ./ COPY --from=builder /app/next.config.js* ./
# Prune dev dependencies
RUN npm prune --production
EXPOSE 3000 EXPOSE 3000
# Run migrations and start # Dynamically set PRISMA_CLI_BINARY_TARGETS based on runtime architecture and start
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"] CMD ["sh", "-c", "\
export PRISMA_CLI_BINARY_TARGETS=$(case $(uname -m) in \
x86_64) echo linux-musl-openssl-3.0.x ;; \
aarch64) echo linux-musl-arm64-openssl-3.0.x ;; \
armv7l) echo linux-musl-arm-openssl-3.0.x ;; \
*) echo \"Unsupported architecture: $(uname -m)\" && exit 1 ;; \
esac) && \
npx prisma migrate deploy && \
npm start"]

View File

@@ -7,6 +7,7 @@
The only dashboard you'll ever need to manage your entire server infrastructure. Keep all your server data organized in one central place, easily add your self-hosted applications with quick access links, and monitor their availability in real-time with built-in uptime tracking. Designed for simplicity and control, it gives you a clear overview of your entire self-hosted setup at a glance. The only dashboard you'll ever need to manage your entire server infrastructure. Keep all your server data organized in one central place, easily add your self-hosted applications with quick access links, and monitor their availability in real-time with built-in uptime tracking. Designed for simplicity and control, it gives you a clear overview of your entire self-hosted setup at a glance.
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/corecontrol) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/corecontrol)
[![Sponsor](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)](https://github.com/sponsors/crocofied)
## Features ## Features
@@ -96,12 +97,12 @@ __Password:__ admin
The application is build with: The application is build with:
- Next.js & Typescript - Next.js & Typescript
- Go (for the agent) - Go (used for the agent)
- Tailwindcss with [shadcn](shadcn.com) - Tailwindcss with [shadcn](shadcn.com)
- PostgreSQL with [Prisma ORM](https://www.prisma.io/) - PostgreSQL with [Prisma ORM](https://www.prisma.io/)
- Icons by [Lucide](https://lucide.dev/) - Icons by [Lucide](https://lucide.dev/)
- Flowcharts by [React Flow](https://reactflow.dev/) - Flowcharts by [React Flow](https://reactflow.dev/)
- Application icons by [selfh.st/icons](selfh.st/icons) - Application icons by [selfh.st/icons](https://selfh.st/icons)
- Monitoring Tool by [Glances](https://github.com/nicolargo/glances) - Monitoring Tool by [Glances](https://github.com/nicolargo/glances)
- and a lot of love ❤️ - and a lot of love ❤️

View File

@@ -1,25 +1,45 @@
# --- Build Stage --- # --- Build Stage ---
FROM golang:1.24-alpine AS builder # Multi-Arch Builder mit expliziter Plattform-Angabe
FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS builder
WORKDIR /app ARG TARGETOS TARGETARCH
ENV GO111MODULE=on WORKDIR /app
COPY go.mod go.sum ./ ENV GO111MODULE=on \
RUN go mod download CGO_ENABLED=0 \
GOOS=$TARGETOS \
GOARCH=$TARGETARCH
COPY . . COPY go.mod go.sum ./
RUN go mod download
RUN go build -o app . COPY . .
# --- Run Stage --- # Cross-Compile für Zielarchitektur
FROM alpine:latest RUN go build -ldflags="-w -s" -o app ./cmd/agent
RUN apk --no-cache add ca-certificates # --- Run Stage ---
# Multi-Arch Laufzeit-Image
FROM alpine:latest
WORKDIR /root/ # Notwendig für TLS/SSL-Zertifikate
RUN apk --no-cache add ca-certificates gcompat
COPY --from=builder /app/app . WORKDIR /root/
CMD ["./app"] 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 \
# .

111
agent/cmd/agent/main.go Normal file
View File

@@ -0,0 +1,111 @@
package main
import (
"fmt"
"net/http"
"time"
"github.com/corecontrol/agent/internal/app"
"github.com/corecontrol/agent/internal/database"
"github.com/corecontrol/agent/internal/notifications"
"github.com/corecontrol/agent/internal/server"
)
func main() {
// Initialize database
db, err := database.InitDB()
if err != nil {
panic(fmt.Sprintf("Database initialization failed: %v\n", err))
}
defer db.Close()
// Initialize notification sender
notifSender := notifications.NewNotificationSender()
// Initial load of notifications
notifs, err := database.LoadNotifications(db)
if err != nil {
panic(fmt.Sprintf("Failed to load notifications: %v", err))
}
notifSender.UpdateNotifications(notifs)
// Reload notification configs every minute
go func() {
reloadTicker := time.NewTicker(time.Minute)
defer reloadTicker.Stop()
for range reloadTicker.C {
newNotifs, err := database.LoadNotifications(db)
if err != nil {
fmt.Printf("Failed to reload notifications: %v\n", err)
continue
}
notifSender.UpdateNotifications(newNotifs)
fmt.Println("Reloaded notification configurations")
}
}()
// Clean up old entries hourly
go func() {
deletionTicker := time.NewTicker(time.Hour)
defer deletionTicker.Stop()
for range deletionTicker.C {
if err := database.DeleteOldEntries(db); err != nil {
fmt.Printf("Error deleting old entries: %v\n", err)
}
}
}()
// Check for test notifications every 10 seconds
go func() {
testNotifTicker := time.NewTicker(10 * time.Second)
defer testNotifTicker.Stop()
for range testNotifTicker.C {
notifs := notifSender.GetNotifications()
database.CheckAndSendTestNotifications(db, notifs, notifSender.SendSpecificNotification)
}
}()
// HTTP clients
appClient := &http.Client{
Timeout: 4 * time.Second,
}
serverClient := &http.Client{
Timeout: 5 * time.Second,
}
// Server monitoring every 5 seconds
go func() {
serverTicker := time.NewTicker(5 * time.Second)
defer serverTicker.Stop()
for range serverTicker.C {
servers, err := database.GetServers(db)
if err != nil {
fmt.Printf("Error getting servers: %v\n", err)
continue
}
server.MonitorServers(db, serverClient, servers, notifSender)
}
}()
// Application monitoring every 10 seconds
appTicker := time.NewTicker(time.Second)
defer appTicker.Stop()
for now := range appTicker.C {
if now.Second()%10 != 0 {
continue
}
apps, err := database.GetApplications(db)
if err != nil {
fmt.Printf("Error getting applications: %v\n", err)
continue
}
app.MonitorApplications(db, appClient, apps, notifSender)
}
}

View File

@@ -1,22 +1,22 @@
module agent module github.com/corecontrol/agent
go 1.24.1 go 1.19
require ( require (
github.com/jackc/pgx/v4 v4.18.3 github.com/jackc/pgx/v4 v4.18.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
) )
require ( require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgtype v1.14.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.7.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
) )

View File

@@ -25,8 +25,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@@ -42,8 +42,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
@@ -57,11 +57,12 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -98,13 +99,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -126,17 +132,22 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -149,15 +160,21 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -165,7 +182,9 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -0,0 +1,130 @@
package app
import (
"context"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/corecontrol/agent/internal/models"
"github.com/corecontrol/agent/internal/notifications"
)
// MonitorApplications checks and updates the status of all applications
func MonitorApplications(db *sql.DB, client *http.Client, apps []models.Application, notifSender *notifications.NotificationSender) {
var notificationTemplate string
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The application !name (!url) went !status!"
}
for _, app := range apps {
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
fmt.Printf("%s Checking...\n", logPrefix)
// Determine which URL to use for monitoring
checkURL := app.PublicURL
if app.UptimeCheckURL != "" {
checkURL = app.UptimeCheckURL
fmt.Printf("%s Using custom uptime check URL: %s\n", logPrefix, checkURL)
}
parsedURL, parseErr := url.Parse(checkURL)
if parseErr != nil {
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
continue
}
hostIsIP := isIPAddress(parsedURL.Hostname())
var isOnline bool
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", checkURL, nil)
if err != nil {
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
continue
}
resp, err := client.Do(req)
if err == nil {
defer resp.Body.Close()
isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400
fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode)
} else {
fmt.Printf("%s Connection error: %v\n", logPrefix, err)
if hostIsIP {
var urlErr *url.Error
if errors.As(err, &urlErr) {
var certErr x509.HostnameError
var unknownAuthErr x509.UnknownAuthorityError
if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) {
fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix)
isOnline = true
}
}
}
}
if isOnline != app.Online {
status := "offline"
if isOnline {
status = "online"
}
message := strings.ReplaceAll(notificationTemplate, "!name", app.Name)
message = strings.ReplaceAll(message, "!url", app.PublicURL)
message = strings.ReplaceAll(message, "!status", status)
notifSender.SendNotifications(message)
}
// Update application status in database
updateApplicationStatus(db, app.ID, isOnline)
// Add entry to uptime history
addUptimeHistoryEntry(db, app.ID, isOnline)
}
}
// Helper function to update application status
func updateApplicationStatus(db *sql.DB, appID int, online bool) {
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer dbCancel()
_, err := db.ExecContext(dbCtx,
`UPDATE application SET online = $1 WHERE id = $2`,
online, appID,
)
if err != nil {
fmt.Printf("DB update failed for app ID %d: %v\n", appID, err)
}
}
// Helper function to add uptime history entry
func addUptimeHistoryEntry(db *sql.DB, appID int, online bool) {
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer dbCancel()
_, err := db.ExecContext(dbCtx,
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
appID, online,
)
if err != nil {
fmt.Printf("History insert failed for app ID %d: %v\n", appID, err)
}
}
// Helper function to check if a host is an IP address
func isIPAddress(host string) bool {
ip := net.ParseIP(host)
return ip != nil
}

View File

@@ -0,0 +1,198 @@
package database
import (
"context"
"database/sql"
"fmt"
"os"
"time"
"github.com/corecontrol/agent/internal/models"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/joho/godotenv"
)
// InitDB initializes the database connection
func InitDB() (*sql.DB, error) {
// Load environment variables
if err := godotenv.Load(); err != nil {
fmt.Println("No env vars found")
}
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
return nil, fmt.Errorf("DATABASE_URL not set")
}
db, err := sql.Open("pgx", dbURL)
if err != nil {
return nil, fmt.Errorf("database connection failed: %v", err)
}
return db, nil
}
// GetApplications fetches all applications with public URLs
func GetApplications(db *sql.DB) ([]models.Application, error) {
rows, err := db.Query(
`SELECT id, name, "publicURL", online, "uptimecheckUrl" FROM application WHERE "publicURL" IS NOT NULL`,
)
if err != nil {
return nil, fmt.Errorf("error fetching applications: %v", err)
}
defer rows.Close()
var apps []models.Application
for rows.Next() {
var app models.Application
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online, &app.UptimeCheckURL); err != nil {
fmt.Printf("Error scanning row: %v\n", err)
continue
}
apps = append(apps, app)
}
return apps, nil
}
// GetServers fetches all servers with monitoring enabled
func GetServers(db *sql.DB) ([]models.Server, error) {
rows, err := db.Query(
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
FROM server WHERE monitoring = true`,
)
if err != nil {
return nil, fmt.Errorf("error fetching servers: %v", err)
}
defer rows.Close()
var servers []models.Server
for rows.Next() {
var server models.Server
if err := rows.Scan(
&server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL,
&server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage,
); err != nil {
fmt.Printf("Error scanning server row: %v\n", err)
continue
}
servers = append(servers, server)
}
return servers, nil
}
// LoadNotifications loads all enabled notifications
func LoadNotifications(db *sql.DB) ([]models.Notification, error) {
rows, err := db.Query(
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken",
"pushoverUrl", "pushoverToken", "pushoverUser", "echobellURL"
FROM notification
WHERE enabled = true`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var configs []models.Notification
for rows.Next() {
var n models.Notification
if err := rows.Scan(
&n.ID, &n.Enabled, &n.Type,
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo,
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook,
&n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
&n.PushoverUrl, &n.PushoverToken, &n.PushoverUser, &n.EchobellURL,
); err != nil {
fmt.Printf("Error scanning notification: %v\n", err)
continue
}
configs = append(configs, n)
}
return configs, nil
}
// DeleteOldEntries removes entries older than 30 days
func DeleteOldEntries(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Delete old uptime history entries
res, err := db.ExecContext(ctx,
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`,
)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
// Delete old server history entries
res, err = db.ExecContext(ctx,
`DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`,
)
if err != nil {
return err
}
affected, _ = res.RowsAffected()
fmt.Printf("Deleted %d old entries from server_history\n", affected)
return nil
}
// UpdateServerStatus updates a server's status and metrics
func UpdateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.ExecContext(ctx,
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5
WHERE id = $6`,
online, cpuUsage, ramUsage, diskUsage, uptime, serverID,
)
return err
}
// CheckAndSendTestNotifications checks for and processes test notifications
func CheckAndSendTestNotifications(db *sql.DB, notifications []models.Notification, sendFunc func(models.Notification, string)) {
// Query for test notifications
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
if err != nil {
fmt.Printf("Error fetching test notifications: %v\n", err)
return
}
defer rows.Close()
// Process each test notification
var testIds []int
for rows.Next() {
var id, notificationId int
if err := rows.Scan(&id, &notificationId); err != nil {
fmt.Printf("Error scanning test notification: %v\n", err)
continue
}
// Add to list of IDs to delete
testIds = append(testIds, id)
// Find the notification configuration
for _, n := range notifications {
if n.ID == notificationId {
// Send test notification
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
sendFunc(n, "Test notification from CoreControl")
}
}
}
// Delete processed test notifications
if len(testIds) > 0 {
for _, id := range testIds {
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
if err != nil {
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
}
}
}
}

View File

@@ -0,0 +1,98 @@
package models
import (
"database/sql"
)
type Application struct {
ID int
Name string
PublicURL string
Online bool
UptimeCheckURL string
}
type Server struct {
ID int
Name string
Monitoring bool
MonitoringURL sql.NullString
Online bool
CpuUsage sql.NullFloat64
RamUsage sql.NullFloat64
DiskUsage sql.NullFloat64
GpuUsage sql.NullFloat64
Temp sql.NullFloat64
Uptime sql.NullString
}
type CPUResponse struct {
Total float64 `json:"total"`
}
type MemoryResponse struct {
Active int64 `json:"active"`
Available int64 `json:"available"`
Buffers int64 `json:"buffers"`
Cached int64 `json:"cached"`
Free int64 `json:"free"`
Inactive int64 `json:"inactive"`
Percent float64 `json:"percent"`
Shared int64 `json:"shared"`
Total int64 `json:"total"`
Used int64 `json:"used"`
}
type FSResponse []struct {
DeviceName string `json:"device_name"`
MntPoint string `json:"mnt_point"`
Percent float64 `json:"percent"`
}
type UptimeResponse struct {
Value string `json:"value"`
}
type 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
Type string
SMTPHost sql.NullString
SMTPPort sql.NullInt64
SMTPFrom sql.NullString
SMTPUser sql.NullString
SMTPPass sql.NullString
SMTPSecure sql.NullBool
SMTPTo sql.NullString
TelegramChatID sql.NullString
TelegramToken sql.NullString
DiscordWebhook sql.NullString
GotifyUrl sql.NullString
GotifyToken sql.NullString
NtfyUrl sql.NullString
NtfyToken sql.NullString
PushoverUrl sql.NullString
PushoverToken sql.NullString
PushoverUser sql.NullString
EchobellURL sql.NullString
}

View File

@@ -0,0 +1,266 @@
package notifications
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/corecontrol/agent/internal/models"
"gopkg.in/gomail.v2"
)
type NotificationSender struct {
notifications []models.Notification
notifMutex sync.RWMutex
}
// NewNotificationSender creates a new notification sender
func NewNotificationSender() *NotificationSender {
return &NotificationSender{
notifications: []models.Notification{},
notifMutex: sync.RWMutex{},
}
}
// UpdateNotifications updates the stored notifications
func (ns *NotificationSender) UpdateNotifications(notifs []models.Notification) {
ns.notifMutex.Lock()
defer ns.notifMutex.Unlock()
copyDst := make([]models.Notification, len(notifs))
copy(copyDst, notifs)
ns.notifications = copyDst
}
// GetNotifications returns a safe copy of current notifications
func (ns *NotificationSender) GetNotifications() []models.Notification {
ns.notifMutex.RLock()
defer ns.notifMutex.RUnlock()
copyDst := make([]models.Notification, len(ns.notifications))
copy(copyDst, ns.notifications)
return copyDst
}
// SendNotifications sends a message to all configured notifications
func (ns *NotificationSender) SendNotifications(message string) {
notifs := ns.GetNotifications()
for _, n := range notifs {
ns.SendSpecificNotification(n, message)
}
}
// SendSpecificNotification sends a message to a specific notification
func (ns *NotificationSender) SendSpecificNotification(n models.Notification, message string) {
fmt.Println("Sending specific notification..." + n.Type)
switch n.Type {
case "smtp":
if n.SMTPHost.Valid && n.SMTPTo.Valid {
ns.sendEmail(n, message)
}
case "telegram":
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
ns.sendTelegram(n, message)
}
case "discord":
if n.DiscordWebhook.Valid {
ns.sendDiscord(n, message)
}
case "gotify":
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
ns.sendGotify(n, message)
}
case "ntfy":
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
ns.sendNtfy(n, message)
}
case "pushover":
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
ns.sendPushover(n, message)
}
case "echobell":
if n.EchobellURL.Valid {
ns.sendEchobell(n, message)
}
}
}
// Helper function to check if a host is an IP address
func (ns *NotificationSender) isIPAddress(host string) bool {
ip := net.ParseIP(host)
return ip != nil
}
// Individual notification methods
func (ns *NotificationSender) sendEmail(n models.Notification, body string) {
// Initialize SMTP dialer with host, port, user, pass
d := gomail.NewDialer(
n.SMTPHost.String,
int(n.SMTPPort.Int64),
n.SMTPUser.String,
n.SMTPPass.String,
)
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
d.SSL = true
}
m := gomail.NewMessage()
m.SetHeader("From", n.SMTPFrom.String)
m.SetHeader("To", n.SMTPTo.String)
m.SetHeader("Subject", "Uptime Notification")
m.SetBody("text/plain", body)
if err := d.DialAndSend(m); err != nil {
fmt.Printf("Email send failed: %v\n", err)
}
}
func (ns *NotificationSender) sendTelegram(n models.Notification, message string) {
apiUrl := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
n.TelegramToken.String,
n.TelegramChatID.String,
message,
)
resp, err := http.Get(apiUrl)
if err != nil {
fmt.Printf("Telegram send failed: %v\n", err)
return
}
resp.Body.Close()
}
func (ns *NotificationSender) sendDiscord(n models.Notification, message string) {
payload := fmt.Sprintf(`{"content": "%s"}`, message)
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
if err != nil {
fmt.Printf("Discord request creation failed: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Discord send failed: %v\n", err)
return
}
resp.Body.Close()
}
func (ns *NotificationSender) sendGotify(n models.Notification, message string) {
baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/")
targetURL := fmt.Sprintf("%s/message", baseURL)
form := url.Values{}
form.Add("message", message)
form.Add("priority", "5")
req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Gotify: ERROR creating request: %v\n", err)
return
}
req.Header.Set("X-Gotify-Key", n.GotifyToken.String)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Gotify: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode)
}
}
func (ns *NotificationSender) sendNtfy(n models.Notification, message string) {
fmt.Println("Sending Ntfy notification...")
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
// Don't append a topic to the URL - the URL itself should have the correct endpoint
requestURL := baseURL
// Send message directly as request body instead of JSON
req, err := http.NewRequest("POST", requestURL, strings.NewReader(message))
if err != nil {
fmt.Printf("Ntfy: ERROR creating request: %v\n", err)
return
}
if n.NtfyToken.Valid {
req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String)
}
// Use text/plain instead of application/json
req.Header.Set("Content-Type", "text/plain")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Ntfy: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
}
}
func (ns *NotificationSender) sendPushover(n models.Notification, message string) {
form := url.Values{}
form.Add("token", n.PushoverToken.String)
form.Add("user", n.PushoverUser.String)
form.Add("message", message)
req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Pushover: ERROR creating request: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Pushover: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
}
}
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)
}
}

View File

@@ -0,0 +1,381 @@
package server
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"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
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The server !name is now !status!"
}
for _, server := range servers {
if !server.Monitoring || !server.MonitoringURL.Valid {
continue
}
logPrefix := fmt.Sprintf("[Server %s]", server.Name)
fmt.Printf("%s Checking...\n", logPrefix)
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
var cpuUsage, ramUsage, diskUsage, 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, 0, 0, "")
if shouldSendNotification(server.ID, online) {
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
}
addServerHistoryEntry(db, server.ID, false, 0, 0, 0, 0, 0)
continue
}
// Get uptime if server is online
uptimeStr = fetchUptime(client, baseURL, logPrefix)
// Get Memory usage
memOnline, memUsage := fetchMemoryUsage(client, baseURL, logPrefix)
if !memOnline {
online = false
updateServerStatus(db, server.ID, false, 0, 0, 0, 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
// Get Disk usage
diskOnline, diskUsageVal := fetchDiskUsage(client, baseURL, logPrefix)
if !diskOnline {
online = false
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 && shouldSendNotification(server.ID, online) {
sendStatusChangeNotification(server, online, notificationTemplate, notifSender)
}
// Update server status with metrics
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, gpuUsage, temp, uptimeStr)
// Add entry to server history
addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage, gpuUsage, temp)
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))
if err != nil {
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
return false, 0
}
defer cpuResp.Body.Close()
if cpuResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
return false, 0
}
var cpuData models.CPUResponse
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
return false, 0
}
return true, cpuData.Total
}
// Helper function to fetch memory usage
func fetchMemoryUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
if err != nil {
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
return false, 0
}
defer memResp.Body.Close()
if memResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
return false, 0
}
var memData models.MemoryResponse
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
return false, 0
}
return true, memData.Percent
}
// Helper function to fetch disk usage
func fetchDiskUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) {
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
if err != nil {
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
return false, 0
}
defer fsResp.Body.Close()
if fsResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
return false, 0
}
var fsData models.FSResponse
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
return false, 0
}
if len(fsData) > 0 {
return true, fsData[0].Percent
}
return true, 0
}
// Helper function to fetch uptime
func fetchUptime(client *http.Client, baseURL, logPrefix string) string {
uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL))
if err != nil || uptimeResp.StatusCode != http.StatusOK {
if err != nil {
fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err)
} else {
fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode)
uptimeResp.Body.Close()
}
return ""
}
defer uptimeResp.Body.Close()
// Read the response body as a string first
uptimeBytes, err := io.ReadAll(uptimeResp.Body)
if err != nil {
fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err)
return ""
}
uptimeStr := strings.Trim(string(uptimeBytes), "\"")
// Try to parse as JSON object first, then fallback to direct string if that fails
var uptimeData models.UptimeResponse
if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" {
uptimeStr = formatUptime(uptimeData.Value)
} else {
// Use the string directly
uptimeStr = formatUptime(uptimeStr)
}
fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr)
return uptimeStr
}
// Helper function to 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"
if online {
status = "online"
}
message := strings.ReplaceAll(template, "!name", server.Name)
message = strings.ReplaceAll(message, "!status", status)
notifSender.SendNotifications(message)
}
// Helper function to update server status
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage, 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, "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)
}
}
// Helper function to add server history entry
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", "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)
}
}
// FormatUptime formats the uptime string to a standard format
func formatUptime(uptimeStr string) string {
// Example input: "3 days, 3:52:36"
// Target output: "28.6 13:52"
now := time.Now()
// Parse the uptime components
parts := strings.Split(uptimeStr, ", ")
var days int
var timeStr string
if len(parts) == 2 {
// Has days part and time part
_, err := fmt.Sscanf(parts[0], "%d days", &days)
if err != nil {
// Try singular "day"
_, err = fmt.Sscanf(parts[0], "%d day", &days)
if err != nil {
return uptimeStr // Return original if parsing fails
}
}
timeStr = parts[1]
} else if len(parts) == 1 {
// Only has time part (less than a day)
days = 0
timeStr = parts[0]
} else {
return uptimeStr // Return original if format is unexpected
}
// Parse the time component (hours:minutes:seconds)
var hours, minutes, seconds int
_, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds)
if err != nil {
return uptimeStr // Return original if parsing fails
}
// Calculate the total duration
duration := time.Duration(days)*24*time.Hour +
time.Duration(hours)*time.Hour +
time.Duration(minutes)*time.Minute +
time.Duration(seconds)*time.Second
// Calculate the start time by subtracting the duration from now
startTime := now.Add(-duration)
// Format the result in the required format (day.month hour:minute)
return startTime.Format("2.1 15:04")
}

View File

@@ -1,805 +0,0 @@
package main
import (
"bytes"
"context"
"crypto/x509"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/joho/godotenv"
"gopkg.in/gomail.v2"
)
type Application struct {
ID int
Name string
PublicURL string
Online bool
}
type Server struct {
ID int
Name string
Monitoring bool
MonitoringURL sql.NullString
Online bool
CpuUsage sql.NullFloat64
RamUsage sql.NullFloat64
DiskUsage sql.NullFloat64
}
type CPUResponse struct {
Total float64 `json:"total"`
}
type MemoryResponse struct {
Active int64 `json:"active"`
Available int64 `json:"available"`
Buffers int64 `json:"buffers"`
Cached int64 `json:"cached"`
Free int64 `json:"free"`
Inactive int64 `json:"inactive"`
Percent float64 `json:"percent"`
Shared int64 `json:"shared"`
Total int64 `json:"total"`
Used int64 `json:"used"`
}
type FSResponse []struct {
DeviceName string `json:"device_name"`
MntPoint string `json:"mnt_point"`
Percent float64 `json:"percent"`
}
type Notification struct {
ID int
Enabled bool
Type string
SMTPHost sql.NullString
SMTPPort sql.NullInt64
SMTPFrom sql.NullString
SMTPUser sql.NullString
SMTPPass sql.NullString
SMTPSecure sql.NullBool
SMTPTo sql.NullString
TelegramChatID sql.NullString
TelegramToken sql.NullString
DiscordWebhook sql.NullString
GotifyUrl sql.NullString
GotifyToken sql.NullString
NtfyUrl sql.NullString
NtfyToken sql.NullString
PushoverUrl sql.NullString
PushoverToken sql.NullString
PushoverUser sql.NullString
}
var (
notifications []Notification
notifMutex sync.RWMutex
)
func main() {
if err := godotenv.Load(); err != nil {
fmt.Println("No env vars found")
}
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
panic("DATABASE_URL not set")
}
db, err := sql.Open("pgx", dbURL)
if err != nil {
panic(fmt.Sprintf("Database connection failed: %v\n", err))
}
defer db.Close()
// initial load
notifs, err := loadNotifications(db)
if err != nil {
panic(fmt.Sprintf("Failed to load notifications: %v", err))
}
notifMutex.Lock()
notifications = notifMutexCopy(notifs)
notifMutex.Unlock()
// reload notification configs every minute
go func() {
reloadTicker := time.NewTicker(time.Minute)
defer reloadTicker.Stop()
for range reloadTicker.C {
newNotifs, err := loadNotifications(db)
if err != nil {
fmt.Printf("Failed to reload notifications: %v\n", err)
continue
}
notifMutex.Lock()
notifications = notifMutexCopy(newNotifs)
notifMutex.Unlock()
fmt.Println("Reloaded notification configurations")
}
}()
// clean up old entries hourly
go func() {
deletionTicker := time.NewTicker(time.Hour)
defer deletionTicker.Stop()
for range deletionTicker.C {
if err := deleteOldEntries(db); err != nil {
fmt.Printf("Error deleting old entries: %v\n", err)
}
}
}()
// Check for test notifications every 10 seconds
go func() {
testNotifTicker := time.NewTicker(10 * time.Second)
defer testNotifTicker.Stop()
for range testNotifTicker.C {
checkAndSendTestNotifications(db)
}
}()
appClient := &http.Client{
Timeout: 4 * time.Second,
}
// Server monitoring every 5 seconds
go func() {
serverClient := &http.Client{
Timeout: 5 * time.Second,
}
serverTicker := time.NewTicker(5 * time.Second)
defer serverTicker.Stop()
for range serverTicker.C {
servers := getServers(db)
checkAndUpdateServerStatus(db, serverClient, servers)
}
}()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for now := range ticker.C {
if now.Second()%10 != 0 {
continue
}
apps := getApplications(db)
checkAndUpdateStatus(db, appClient, apps)
}
}
// helper to safely copy slice
func notifMutexCopy(src []Notification) []Notification {
copyDst := make([]Notification, len(src))
copy(copyDst, src)
return copyDst
}
func isIPAddress(host string) bool {
ip := net.ParseIP(host)
return ip != nil
}
func loadNotifications(db *sql.DB) ([]Notification, error) {
rows, err := db.Query(
`SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo",
"telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken"
FROM notification
WHERE enabled = true`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var configs []Notification
for rows.Next() {
var n Notification
if err := rows.Scan(
&n.ID, &n.Enabled, &n.Type,
&n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo,
&n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken,
); err != nil {
fmt.Printf("Error scanning notification: %v\n", err)
continue
}
configs = append(configs, n)
}
return configs, nil
}
func deleteOldEntries(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Delete old uptime history entries
res, err := db.ExecContext(ctx,
`DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`,
)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
fmt.Printf("Deleted %d old entries from uptime_history\n", affected)
// Delete old server history entries
res, err = db.ExecContext(ctx,
`DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`,
)
if err != nil {
return err
}
affected, _ = res.RowsAffected()
fmt.Printf("Deleted %d old entries from server_history\n", affected)
return nil
}
func getApplications(db *sql.DB) []Application {
rows, err := db.Query(
`SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`,
)
if err != nil {
fmt.Printf("Error fetching applications: %v\n", err)
return nil
}
defer rows.Close()
var apps []Application
for rows.Next() {
var app Application
if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil {
fmt.Printf("Error scanning row: %v\n", err)
continue
}
apps = append(apps, app)
}
return apps
}
func getServers(db *sql.DB) []Server {
rows, err := db.Query(
`SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage"
FROM server WHERE monitoring = true`,
)
if err != nil {
fmt.Printf("Error fetching servers: %v\n", err)
return nil
}
defer rows.Close()
var servers []Server
for rows.Next() {
var server Server
if err := rows.Scan(
&server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL,
&server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage,
); err != nil {
fmt.Printf("Error scanning server row: %v\n", err)
continue
}
servers = append(servers, server)
}
return servers
}
func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) {
var notificationTemplate string
err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The application !name (!url) went !status!"
}
for _, app := range apps {
logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL)
fmt.Printf("%s Checking...\n", logPrefix)
parsedURL, parseErr := url.Parse(app.PublicURL)
if parseErr != nil {
fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr)
continue
}
hostIsIP := isIPAddress(parsedURL.Hostname())
var isOnline bool
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", app.PublicURL, nil)
if err != nil {
fmt.Printf("%s Request creation failed: %v\n", logPrefix, err)
continue
}
resp, err := client.Do(req)
if err == nil {
defer resp.Body.Close()
isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400
fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode)
} else {
fmt.Printf("%s Connection error: %v\n", logPrefix, err)
if hostIsIP {
var urlErr *url.Error
if errors.As(err, &urlErr) {
var certErr x509.HostnameError
var unknownAuthErr x509.UnknownAuthorityError
if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) {
fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix)
isOnline = true
}
}
}
}
if isOnline != app.Online {
status := "offline"
if isOnline {
status = "online"
}
message := strings.ReplaceAll(notificationTemplate, "!name", app.Name)
message = strings.ReplaceAll(message, "!url", app.PublicURL)
message = strings.ReplaceAll(message, "!status", status)
sendNotifications(message)
}
dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = db.ExecContext(dbCtx,
`UPDATE application SET online = $1 WHERE id = $2`,
isOnline, app.ID,
)
if err != nil {
fmt.Printf("%s DB update failed: %v\n", logPrefix, err)
}
dbCancel()
dbCtx2, dbCancel2 := context.WithTimeout(context.Background(), 5*time.Second)
_, err = db.ExecContext(dbCtx2,
`INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`,
app.ID, isOnline,
)
if err != nil {
fmt.Printf("%s History insert failed: %v\n", logPrefix, err)
}
dbCancel2()
}
}
func checkAndUpdateServerStatus(db *sql.DB, client *http.Client, servers []Server) {
var notificationTemplate string
err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(&notificationTemplate)
if err != nil || notificationTemplate == "" {
notificationTemplate = "The server !name is now !status!"
}
for _, server := range servers {
if !server.Monitoring || !server.MonitoringURL.Valid {
continue
}
logPrefix := fmt.Sprintf("[Server %s]", server.Name)
fmt.Printf("%s Checking...\n", logPrefix)
baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/")
var cpuUsage, ramUsage, diskUsage float64
var online = true
// Get CPU usage
cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL))
if err != nil {
fmt.Printf("%s CPU request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer cpuResp.Body.Close()
if cpuResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var cpuData CPUResponse
if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil {
fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
cpuUsage = cpuData.Total
}
}
}
if online {
// Get Memory usage
memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL))
if err != nil {
fmt.Printf("%s Memory request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer memResp.Body.Close()
if memResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var memData MemoryResponse
if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil {
fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
// Calculate actual RAM usage excluding swap, cache, and buffers
// Formula: (total - free - cached - buffers) / total * 100
// This is the most accurate representation of actual used RAM
actualUsedRam := memData.Total - memData.Free - memData.Cached - memData.Buffers
if actualUsedRam < 0 {
actualUsedRam = 0 // Safeguard against negative values
}
if memData.Total > 0 {
ramUsage = float64(actualUsedRam) / float64(memData.Total) * 100
fmt.Printf("%s Calculated RAM usage: %.2f%% (Used: %d MB, Total: %d MB)\n",
logPrefix, ramUsage, actualUsedRam/1024/1024, memData.Total/1024/1024)
} else {
// Fallback to the provided percentage if calculation fails
ramUsage = memData.Percent
fmt.Printf("%s Using provided memory percentage because total is zero\n", logPrefix)
}
}
}
}
}
if online {
// Get Disk usage
fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL))
if err != nil {
fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
defer fsResp.Body.Close()
if fsResp.StatusCode != http.StatusOK {
fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else {
var fsData FSResponse
if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil {
fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err)
updateServerStatus(db, server.ID, false, 0, 0, 0)
online = false
} else if len(fsData) > 0 {
diskUsage = fsData[0].Percent
}
}
}
}
// Check if status changed and send notification if needed
if online != server.Online {
status := "offline"
if online {
status = "online"
}
message := notificationTemplate
message = strings.ReplaceAll(message, "!name", server.Name)
message = strings.ReplaceAll(message, "!status", status)
sendNotifications(message)
}
// Update server status with metrics
updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage)
// Add entry to server history
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = db.ExecContext(ctx,
`INSERT INTO server_history(
"serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt"
) VALUES ($1, $2, $3, $4, $5, now())`,
server.ID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage),
)
cancel()
if err != nil {
fmt.Printf("%s Failed to insert history: %v\n", logPrefix, err)
}
fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%\n",
logPrefix, cpuUsage, ramUsage, diskUsage)
}
}
func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.ExecContext(ctx,
`UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8
WHERE id = $5`,
online, cpuUsage, ramUsage, diskUsage, serverID,
)
if err != nil {
fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err)
}
}
func sendNotifications(message string) {
notifMutex.RLock()
notifs := notifMutexCopy(notifications)
notifMutex.RUnlock()
for _, n := range notifs {
switch n.Type {
case "email":
if n.SMTPHost.Valid && n.SMTPTo.Valid {
sendEmail(n, message)
}
case "telegram":
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
sendTelegram(n, message)
}
case "discord":
if n.DiscordWebhook.Valid {
sendDiscord(n, message)
}
case "gotify":
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
sendGotify(n, message)
}
case "ntfy":
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
sendNtfy(n, message)
}
case "pushover":
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
sendPushover(n, message)
}
}
}
}
func sendEmail(n Notification, body string) {
// Initialize SMTP dialer with host, port, user, pass
d := gomail.NewDialer(
n.SMTPHost.String,
int(n.SMTPPort.Int64),
n.SMTPUser.String,
n.SMTPPass.String,
)
if n.SMTPSecure.Valid && n.SMTPSecure.Bool {
d.SSL = true
}
m := gomail.NewMessage()
m.SetHeader("From", n.SMTPFrom.String)
m.SetHeader("To", n.SMTPTo.String)
m.SetHeader("Subject", "Uptime Notification")
m.SetBody("text/plain", body)
if err := d.DialAndSend(m); err != nil {
fmt.Printf("Email send failed: %v\n", err)
}
}
func sendTelegram(n Notification, message string) {
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s",
n.TelegramToken.String,
n.TelegramChatID.String,
message,
)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Telegram send failed: %v\n", err)
return
}
resp.Body.Close()
}
func sendDiscord(n Notification, message string) {
payload := fmt.Sprintf(`{"content": "%s"}`, message)
req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload))
if err != nil {
fmt.Printf("Discord request creation failed: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Discord send failed: %v\n", err)
return
}
resp.Body.Close()
}
func sendGotify(n Notification, message string) {
baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/")
targetURL := fmt.Sprintf("%s/message", baseURL)
form := url.Values{}
form.Add("message", message)
form.Add("priority", "5")
req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Gotify: ERROR creating request: %v\n", err)
return
}
req.Header.Set("X-Gotify-Key", n.GotifyToken.String)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Gotify: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode)
}
}
func sendNtfy(n Notification, message string) {
baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/")
topic := "corecontrol"
requestURL := fmt.Sprintf("%s/%s", baseURL, topic)
payload := map[string]string{"message": message}
jsonData, err := json.Marshal(payload)
if err != nil {
fmt.Printf("Ntfy: ERROR marshaling JSON: %v\n", err)
return
}
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("Ntfy: ERROR creating request: %v\n", err)
return
}
if n.NtfyToken.Valid {
req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Ntfy: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode)
}
}
func sendPushover(n Notification, message string) {
form := url.Values{}
form.Add("token", n.PushoverToken.String)
form.Add("user", n.PushoverUser.String)
form.Add("message", message)
req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode()))
if err != nil {
fmt.Printf("Pushover: ERROR creating request: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Pushover: ERROR sending request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode)
}
}
func checkAndSendTestNotifications(db *sql.DB) {
// Query for test notifications
rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`)
if err != nil {
fmt.Printf("Error fetching test notifications: %v\n", err)
return
}
defer rows.Close()
// Process each test notification
var testIds []int
for rows.Next() {
var id, notificationId int
if err := rows.Scan(&id, &notificationId); err != nil {
fmt.Printf("Error scanning test notification: %v\n", err)
continue
}
// Add to list of IDs to delete
testIds = append(testIds, id)
// Find the notification configuration
notifMutex.RLock()
for _, n := range notifications {
if n.ID == notificationId {
// Send test notification
fmt.Printf("Sending test notification to notification ID %d\n", notificationId)
sendSpecificNotification(n, "Test notification from CoreControl")
}
}
notifMutex.RUnlock()
}
// Delete processed test notifications
if len(testIds) > 0 {
for _, id := range testIds {
_, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id)
if err != nil {
fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err)
}
}
}
}
func sendSpecificNotification(n Notification, message string) {
switch n.Type {
case "email":
if n.SMTPHost.Valid && n.SMTPTo.Valid {
sendEmail(n, message)
}
case "telegram":
if n.TelegramToken.Valid && n.TelegramChatID.Valid {
sendTelegram(n, message)
}
case "discord":
if n.DiscordWebhook.Valid {
sendDiscord(n, message)
}
case "gotify":
if n.GotifyUrl.Valid && n.GotifyToken.Valid {
sendGotify(n, message)
}
case "ntfy":
if n.NtfyUrl.Valid && n.NtfyToken.Valid {
sendNtfy(n, message)
}
case "pushover":
if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid {
sendPushover(n, message)
}
}
}

View File

@@ -8,12 +8,13 @@ interface AddRequest {
icon: string; icon: string;
publicURL: string; publicURL: string;
localURL: string; localURL: string;
uptimecheckUrl: string;
} }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body: AddRequest = await request.json(); const body: AddRequest = await request.json();
const { serverId, name, description, icon, publicURL, localURL } = body; const { serverId, name, description, icon, publicURL, localURL, uptimecheckUrl } = body;
const application = await prisma.application.create({ const application = await prisma.application.create({
data: { data: {
@@ -22,7 +23,8 @@ export async function POST(request: NextRequest) {
description, description,
icon, icon,
publicURL, publicURL,
localURL localURL,
uptimecheckUrl
} }
}); });

View File

@@ -9,12 +9,13 @@ interface EditRequest {
icon: string; icon: string;
publicURL: string; publicURL: string;
localURL: string; localURL: string;
uptimecheckUrl: string;
} }
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const body: EditRequest = await request.json(); const body: EditRequest = await request.json();
const { id, name, description, serverId, icon, publicURL, localURL } = body; const { id, name, description, serverId, icon, publicURL, localURL, uptimecheckUrl } = body;
const existingApp = await prisma.application.findUnique({ where: { id } }); const existingApp = await prisma.application.findUnique({ where: { id } });
if (!existingApp) { if (!existingApp) {
@@ -29,7 +30,8 @@ export async function PUT(request: NextRequest) {
description, description,
icon, icon,
publicURL, publicURL,
localURL localURL,
uptimecheckUrl
} }
}); });

View File

@@ -40,7 +40,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
applications: applicationsWithServers, applications: applicationsWithServers,
servers: servers_all, servers: servers_all,
maxPage maxPage,
totalItems: totalCount
}); });
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";

View File

@@ -23,7 +23,23 @@ export async function POST(request: NextRequest) {
const searchResults = fuse.search(searchterm); const searchResults = fuse.search(searchterm);
const results = searchResults.map(({ item }) => item); const searchedApps = searchResults.map(({ item }) => item);
// Get server IDs from the search results
const serverIds = searchedApps
.map(app => app.serverId)
.filter((id): id is number => id !== null);
// Fetch server data for these applications
const servers = await prisma.server.findMany({
where: { id: { in: serverIds } }
});
// Add server name to each application
const results = searchedApps.map(app => ({
...app,
server: servers.find(s => s.id === app.serverId)?.name || "No server"
}));
return NextResponse.json({ results }); return NextResponse.json({ results });
} catch (error: any) { } catch (error: any) {

View File

@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
interface RequestBody { interface RequestBody {
timespan?: number; timespan?: number;
page?: number; page?: number;
itemsPerPage?: number;
} }
@@ -100,8 +101,7 @@ const getIntervalKey = (date: Date, timespan: number) => {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { timespan = 1, page = 1 }: RequestBody = await request.json(); const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json();
const itemsPerPage = 5;
const skip = (page - 1) * itemsPerPage; const skip = (page - 1) * itemsPerPage;
// Get paginated and sorted applications // Get paginated and sorted applications

View File

@@ -1,7 +1,7 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import bcrypt from 'bcrypt'; import bcrypt from 'bcryptjs';
interface EditEmailRequest { interface EditEmailRequest {
oldPassword: string; oldPassword: string;

View File

@@ -1,7 +1,7 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import bcrypt from 'bcrypt'; import bcrypt from 'bcryptjs';
interface LoginRequest { interface LoginRequest {
username: string; username: string;

View File

@@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
interface AddRequest { interface AddRequest {
type: string; type: string;
name: string;
smtpHost?: string; smtpHost?: string;
smtpPort?: number; smtpPort?: number;
smtpSecure?: boolean; smtpSecure?: boolean;
@@ -20,16 +21,18 @@ interface AddRequest {
pushoverUrl?: string; pushoverUrl?: string;
pushoverToken?: string; pushoverToken?: string;
pushoverUser?: string; pushoverUser?: string;
echobellURL?: string;
} }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body: AddRequest = await request.json(); const body: AddRequest = await request.json();
const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser } = body; const { type, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser, echobellURL } = body;
const notification = await prisma.notification.create({ const notification = await prisma.notification.create({
data: { data: {
type: type, type: type,
name: name,
smtpHost: smtpHost, smtpHost: smtpHost,
smtpPort: smtpPort, smtpPort: smtpPort,
smtpFrom: smtpFrom, smtpFrom: smtpFrom,
@@ -47,6 +50,7 @@ export async function POST(request: NextRequest) {
pushoverUrl: pushoverUrl, pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken, pushoverToken: pushoverToken,
pushoverUser: pushoverUser, pushoverUser: pushoverUser,
echobellURL: echobellURL
} }
}); });

View File

@@ -132,6 +132,8 @@ export async function POST(request: NextRequest) {
cpu: number[], cpu: number[],
ram: number[], ram: number[],
disk: number[], disk: number[],
gpu: number[],
temp: number[],
online: boolean[] online: boolean[]
}>(); }>();
@@ -142,6 +144,8 @@ export async function POST(request: NextRequest) {
cpu: [], cpu: [],
ram: [], ram: [],
disk: [], disk: [],
gpu: [],
temp: [],
online: [] online: []
}); });
}); });
@@ -167,6 +171,8 @@ export async function POST(request: NextRequest) {
interval.cpu.push(parseUsageValue(record.cpuUsage)); interval.cpu.push(parseUsageValue(record.cpuUsage));
interval.ram.push(parseUsageValue(record.ramUsage)); interval.ram.push(parseUsageValue(record.ramUsage));
interval.disk.push(parseUsageValue(record.diskUsage)); interval.disk.push(parseUsageValue(record.diskUsage));
interval.gpu.push(parseUsageValue(record.gpuUsage));
interval.temp.push(parseUsageValue(record.temp));
interval.online.push(record.online); interval.online.push(record.online);
} }
}); });
@@ -178,6 +184,8 @@ export async function POST(request: NextRequest) {
cpu: [], cpu: [],
ram: [], ram: [],
disk: [], disk: [],
gpu: [],
temp: [],
online: [] online: []
}; };
@@ -189,6 +197,8 @@ export async function POST(request: NextRequest) {
cpu: average(data.cpu), cpu: average(data.cpu),
ram: average(data.ram), ram: average(data.ram),
disk: average(data.disk), disk: average(data.disk),
gpu: average(data.gpu),
temp: average(data.temp),
online: data.online.length ? online: data.online.length ?
data.online.filter(Boolean).length / data.online.length >= 0.5 data.online.filter(Boolean).length / data.online.length >= 0.5
: null : null
@@ -221,6 +231,14 @@ export async function POST(request: NextRequest) {
const data = historyMap.get(d.toISOString())?.disk || []; const data = historyMap.get(d.toISOString())?.disk || [];
return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; 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 => { online: intervals.map(d => {
const data = historyMap.get(d.toISOString())?.online || []; const data = historyMap.get(d.toISOString())?.online || [];
return data.length ? data.filter(Boolean).length / data.length >= 0.5 : null; return data.length ? data.filter(Boolean).length / data.length >= 0.5 : null;
@@ -233,8 +251,9 @@ export async function POST(request: NextRequest) {
// Only calculate maxPage when not requesting a specific server // Only calculate maxPage when not requesting a specific server
let maxPage = 1; let maxPage = 1;
let totalHosts = 0;
if (!serverId) { if (!serverId) {
const totalHosts = await prisma.server.count({ totalHosts = await prisma.server.count({
where: { OR: [{ hostServer: 0 }, { hostServer: null }] } where: { OR: [{ hostServer: 0 }, { hostServer: null }] }
}); });
maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
@@ -242,7 +261,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
servers: hostsWithVms, servers: hostsWithVms,
maxPage maxPage,
totalItems: totalHosts
}); });
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });

View File

@@ -11,6 +11,9 @@ export async function GET() {
cpuUsage: true, cpuUsage: true,
ramUsage: true, ramUsage: true,
diskUsage: true, diskUsage: true,
gpuUsage: true,
temp: true,
uptime: true
} }
}); });
@@ -20,12 +23,18 @@ export async function GET() {
cpuUsage: string | null; cpuUsage: string | null;
ramUsage: string | null; ramUsage: string | null;
diskUsage: string | null; diskUsage: string | null;
gpuUsage: string | null;
temp: string | null;
uptime: string | null;
}) => ({ }) => ({
id: server.id, id: server.id,
online: server.online, online: server.online,
cpuUsage: server.cpuUsage ? parseInt(server.cpuUsage) : 0, cpuUsage: server.cpuUsage ? parseFloat(server.cpuUsage) : 0,
ramUsage: server.ramUsage ? parseInt(server.ramUsage) : 0, ramUsage: server.ramUsage ? parseFloat(server.ramUsage) : 0,
diskUsage: server.diskUsage ? parseInt(server.diskUsage) : 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 || ""
})); }));
return NextResponse.json(monitoringData) return NextResponse.json(monitoringData)

View File

@@ -17,6 +17,7 @@ import { Separator } from "@/components/ui/separator"
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { useTranslations } from "next-intl"
interface StatsResponse { interface StatsResponse {
serverCountNoVMs: number serverCountNoVMs: number
@@ -26,6 +27,7 @@ interface StatsResponse {
} }
export default function Dashboard() { export default function Dashboard() {
const t = useTranslations('Dashboard')
const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0) const [serverCountNoVMs, setServerCountNoVMs] = useState<number>(0)
const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0) const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState<number>(0)
const [applicationCount, setApplicationCount] = useState<number>(0) const [applicationCount, setApplicationCount] = useState<number>(0)
@@ -62,22 +64,22 @@ export default function Dashboard() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Dashboard</BreadcrumbPage> <BreadcrumbPage>{t('Title')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</div> </div>
</header> </header>
<div className="p-6"> <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"> <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"> <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"> <CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-2xl font-semibold">Servers</CardTitle> <CardTitle className="text-2xl font-semibold">{t('Servers.Title')}</CardTitle>
<CardDescription className="mt-1">Physical and virtual servers overview</CardDescription> <CardDescription className="mt-1">{t('Servers.Description')}</CardDescription>
</div> </div>
<Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" /> <Server className="h-8 w-8 text-rose-500 p-1.5 rounded-lg" />
</div> </div>
@@ -91,7 +93,7 @@ export default function Dashboard() {
</div> </div>
<div> <div>
<div className="text-3xl font-bold">{serverCountNoVMs}</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>
</div> </div>
@@ -102,7 +104,7 @@ export default function Dashboard() {
</div> </div>
<div> <div>
<div className="text-3xl font-bold">{serverCountOnlyVMs}</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> </div>
</div> </div>
@@ -115,7 +117,7 @@ export default function Dashboard() {
asChild asChild
> >
<Link href="/dashboard/servers" className="flex items-center justify-between"> <Link href="/dashboard/servers" className="flex items-center justify-between">
<span>Manage Servers</span> <span>{t('Servers.ManageServers')}</span>
</Link> </Link>
</Button> </Button>
</CardFooter> </CardFooter>
@@ -125,15 +127,15 @@ export default function Dashboard() {
<CardHeader className="py-3 pb-1"> <CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-2xl font-semibold">Applications</CardTitle> <CardTitle className="text-2xl font-semibold">{t('Applications.Title')}</CardTitle>
<CardDescription className="mt-1">Manage your deployed applications</CardDescription> <CardDescription className="mt-1">{t('Applications.Description')}</CardDescription>
</div> </div>
<Layers className="h-8 w-8 text-amber-500 p-1.5 rounded-lg" /> <Layers className="h-8 w-8 text-amber-500 p-1.5 rounded-lg" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-1 pb-2 min-h-[120px]"> <CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="text-4xl font-bold">{applicationCount}</div> <div className="text-4xl font-bold">{applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Running applications</p> <p className="text-sm text-muted-foreground mt-2">{t('Applications.OnlineApplications')}</p>
</CardContent> </CardContent>
<CardFooter className="border-t bg-muted/10 py-2 px-4"> <CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button <Button
@@ -143,7 +145,7 @@ export default function Dashboard() {
asChild asChild
> >
<Link href="/dashboard/applications" className="flex items-center justify-between"> <Link href="/dashboard/applications" className="flex items-center justify-between">
<span>View all applications</span> <span>{t('Applications.ViewAllApplications')}</span>
</Link> </Link>
</Button> </Button>
</CardFooter> </CardFooter>
@@ -153,8 +155,8 @@ export default function Dashboard() {
<CardHeader className="py-3 pb-1"> <CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-2xl font-semibold">Uptime</CardTitle> <CardTitle className="text-2xl font-semibold">{t('Uptime.Title')}</CardTitle>
<CardDescription className="mt-1">Monitor your service availability</CardDescription> <CardDescription className="mt-1">{t('Uptime.Description')}</CardDescription>
</div> </div>
<Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" /> <Activity className="h-8 w-8 text-emerald-500 p-1.5 rounded-lg" />
</div> </div>
@@ -177,7 +179,7 @@ export default function Dashboard() {
}} }}
></div> ></div>
</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> </div>
</CardContent> </CardContent>
<CardFooter className="border-t bg-muted/10 py-2 px-4"> <CardFooter className="border-t bg-muted/10 py-2 px-4">
@@ -188,7 +190,7 @@ export default function Dashboard() {
asChild asChild
> >
<Link href="/dashboard/uptime" className="flex items-center justify-between"> <Link href="/dashboard/uptime" className="flex items-center justify-between">
<span>View uptime metrics</span> <span>{t('Uptime.ViewUptimeMetrics')}</span>
</Link> </Link>
</Button> </Button>
</CardFooter> </CardFooter>
@@ -198,15 +200,15 @@ export default function Dashboard() {
<CardHeader className="py-3 pb-1"> <CardHeader className="py-3 pb-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-2xl font-semibold">Network</CardTitle> <CardTitle className="text-2xl font-semibold">{t('Network.Title')}</CardTitle>
<CardDescription className="mt-1">Manage network configuration</CardDescription> <CardDescription className="mt-1">{t('Network.Description')}</CardDescription>
</div> </div>
<Network className="h-8 w-8 text-sky-500 p-1.5 rounded-lg" /> <Network className="h-8 w-8 text-sky-500 p-1.5 rounded-lg" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-1 pb-2 min-h-[120px]"> <CardContent className="pt-1 pb-2 min-h-[120px]">
<div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div> <div className="text-4xl font-bold">{serverCountNoVMs + serverCountOnlyVMs + applicationCount}</div>
<p className="text-sm text-muted-foreground mt-2">Active connections</p> <p className="text-sm text-muted-foreground mt-2">{t('Network.ActiveConnections')}</p>
</CardContent> </CardContent>
<CardFooter className="border-t bg-muted/10 py-2 px-4"> <CardFooter className="border-t bg-muted/10 py-2 px-4">
<Button <Button
@@ -216,7 +218,7 @@ export default function Dashboard() {
asChild asChild
> >
<Link href="/dashboard/network" className="flex items-center justify-between"> <Link href="/dashboard/network" className="flex items-center justify-between">
<span>View network details</span> <span>{t('Network.ViewNetworkDetails')}</span>
</Link> </Link>
</Button> </Button>
</CardFooter> </CardFooter>

View File

@@ -25,6 +25,9 @@ import {
List, List,
Pencil, Pencil,
Zap, Zap,
ViewIcon,
Grid3X3,
HelpCircle,
} from "lucide-react"; } from "lucide-react";
import { import {
Card, Card,
@@ -65,7 +68,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import axios from "axios"; import axios from "axios";
import { import {
Tooltip, Tooltip,
@@ -76,6 +79,13 @@ import {
import { StatusIndicator } from "@/components/status-indicator"; import { StatusIndicator } from "@/components/status-indicator";
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner" import { toast } from "sonner"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslations } from "next-intl";
interface Application { interface Application {
id: number; id: number;
@@ -87,6 +97,7 @@ interface Application {
server?: string; server?: string;
online: boolean; online: boolean;
serverId: number; serverId: number;
uptimecheckUrl?: string;
} }
interface Server { interface Server {
@@ -98,15 +109,19 @@ interface ApplicationsResponse {
applications: Application[]; applications: Application[];
servers: Server[]; servers: Server[];
maxPage: number; maxPage: number;
totalItems?: number;
} }
export default function Dashboard() { export default function Dashboard() {
const t = useTranslations();
const [name, setName] = useState<string>(""); const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>(""); const [description, setDescription] = useState<string>("");
const [icon, setIcon] = useState<string>(""); const [icon, setIcon] = useState<string>("");
const [publicURL, setPublicURL] = useState<string>(""); const [publicURL, setPublicURL] = useState<string>("");
const [localURL, setLocalURL] = useState<string>(""); const [localURL, setLocalURL] = useState<string>("");
const [serverId, setServerId] = useState<number | null>(null); const [serverId, setServerId] = useState<number | null>(null);
const [customUptimeCheck, setCustomUptimeCheck] = useState<boolean>(false);
const [uptimecheckUrl, setUptimecheckUrl] = useState<string>("");
const [editName, setEditName] = useState<string>(""); const [editName, setEditName] = useState<string>("");
const [editDescription, setEditDescription] = useState<string>(""); const [editDescription, setEditDescription] = useState<string>("");
@@ -115,6 +130,8 @@ export default function Dashboard() {
const [editLocalURL, setEditLocalURL] = useState<string>(""); const [editLocalURL, setEditLocalURL] = useState<string>("");
const [editId, setEditId] = useState<number | null>(null); const [editId, setEditId] = useState<number | null>(null);
const [editServerId, setEditServerId] = useState<number | null>(null); const [editServerId, setEditServerId] = useState<number | null>(null);
const [editCustomUptimeCheck, setEditCustomUptimeCheck] = useState<boolean>(false);
const [editUptimecheckUrl, setEditUptimecheckUrl] = useState<string>("");
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [maxPage, setMaxPage] = useState<number>(1); const [maxPage, setMaxPage] = useState<number>(1);
@@ -126,21 +143,69 @@ export default function Dashboard() {
const [isSearching, setIsSearching] = useState<boolean>(false); const [isSearching, setIsSearching] = useState<boolean>(false);
const savedLayout = Cookies.get("layoutPreference-app"); const savedLayout = Cookies.get("layoutPreference-app");
const savedItemsPerPage = Cookies.get("itemsPerPage-app");
const initialIsGridLayout = savedLayout === "grid"; const initialIsGridLayout = savedLayout === "grid";
const initialItemsPerPage = initialIsGridLayout ? 15 : 5; const initialIsCompactLayout = savedLayout === "compact";
const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5);
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout); const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [isCompactLayout, setIsCompactLayout] = useState<boolean>(initialIsCompactLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage); const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const toggleLayout = () => { const toggleLayout = (layout: string) => {
const newLayout = !isGridLayout; if (layout === "standard") {
setIsGridLayout(newLayout); setIsGridLayout(false);
Cookies.set("layoutPreference-app", newLayout ? "grid" : "standard", { setIsCompactLayout(false);
Cookies.set("layoutPreference-app", "standard", {
expires: 365, expires: 365,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
}); });
setItemsPerPage(newLayout ? 15 : 5); } else if (layout === "grid") {
setIsGridLayout(true);
setIsCompactLayout(false);
Cookies.set("layoutPreference-app", "grid", {
expires: 365,
path: "/",
sameSite: "strict",
});
} else if (layout === "compact") {
setIsGridLayout(false);
setIsCompactLayout(true);
Cookies.set("layoutPreference-app", "compact", {
expires: 365,
path: "/",
sameSite: "strict",
});
}
};
const handleItemsPerPageChange = (value: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
toast.error(t('Applications.Messages.NumberValidation'));
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1);
Cookies.set("itemsPerPage-app", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
}, 300);
}; };
const add = async () => { const add = async () => {
@@ -152,12 +217,13 @@ export default function Dashboard() {
publicURL, publicURL,
localURL, localURL,
serverId, serverId,
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
}); });
getApplications(); getApplications();
toast.success("Application added successfully"); toast.success(t('Applications.Messages.AddSuccess'));
} catch (error: any) { } catch (error: any) {
console.log(error.response?.data); console.log(error.response?.data);
toast.error("Failed to add application"); toast.error(t('Applications.Messages.AddError'));
} }
}; };
@@ -171,13 +237,21 @@ export default function Dashboard() {
setApplications(response.data.applications); setApplications(response.data.applications);
setServers(response.data.servers); setServers(response.data.servers);
setMaxPage(response.data.maxPage); setMaxPage(response.data.maxPage);
if (response.data.totalItems !== undefined) {
setTotalItems(response.data.totalItems);
}
setLoading(false); setLoading(false);
} catch (error: any) { } catch (error: any) {
console.log(error.response?.data); console.log(error.response?.data);
toast.error("Failed to get applications"); toast.error(t('Applications.Messages.GetError'));
} }
}; };
// Calculate current range of items being displayed
const [totalItems, setTotalItems] = useState<number>(0);
const startItem = (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
useEffect(() => { useEffect(() => {
getApplications(); getApplications();
}, [currentPage, itemsPerPage]); }, [currentPage, itemsPerPage]);
@@ -190,10 +264,10 @@ export default function Dashboard() {
try { try {
await axios.post("/api/applications/delete", { id }); await axios.post("/api/applications/delete", { id });
getApplications(); getApplications();
toast.success("Application deleted successfully"); toast.success(t('Applications.Messages.DeleteSuccess'));
} catch (error: any) { } catch (error: any) {
console.log(error.response?.data); console.log(error.response?.data);
toast.error("Failed to delete application"); toast.error(t('Applications.Messages.DeleteError'));
} }
}; };
@@ -205,6 +279,14 @@ export default function Dashboard() {
setEditIcon(app.icon || ""); setEditIcon(app.icon || "");
setEditLocalURL(app.localURL || ""); setEditLocalURL(app.localURL || "");
setEditPublicURL(app.publicURL || ""); setEditPublicURL(app.publicURL || "");
if (app.uptimecheckUrl) {
setEditCustomUptimeCheck(true);
setEditUptimecheckUrl(app.uptimecheckUrl);
} else {
setEditCustomUptimeCheck(false);
setEditUptimecheckUrl("");
}
}; };
const edit = async () => { const edit = async () => {
@@ -219,13 +301,14 @@ export default function Dashboard() {
icon: editIcon, icon: editIcon,
publicURL: editPublicURL, publicURL: editPublicURL,
localURL: editLocalURL, localURL: editLocalURL,
uptimecheckUrl: editCustomUptimeCheck ? editUptimecheckUrl : "",
}); });
getApplications(); getApplications();
setEditId(null); setEditId(null);
toast.success("Application edited successfully"); toast.success(t('Applications.Messages.EditSuccess'));
} catch (error: any) { } catch (error: any) {
console.log(error.response.data); console.log(error.response.data);
toast.error("Failed to edit application"); toast.error(t('Applications.Messages.EditError'));
} }
}; };
@@ -279,11 +362,11 @@ export default function Dashboard() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>My Infrastructure</BreadcrumbPage> <BreadcrumbPage>{t('Applications.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Applications</BreadcrumbPage> <BreadcrumbPage>{t('Applications.Breadcrumb.Applications')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
@@ -292,25 +375,120 @@ export default function Dashboard() {
<Toaster /> <Toaster />
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-3xl font-bold">Your Applications</span> <span className="text-3xl font-bold">{t('Applications.Title')}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <DropdownMenu>
variant="outline" <DropdownMenuTrigger asChild>
size="icon" <Button variant="outline" size="icon" title={t('Applications.Views.ChangeView')}>
onClick={toggleLayout} {isCompactLayout ? (
title={ <Grid3X3 className="h-4 w-4" />
isGridLayout ? "Switch to list view" : "Switch to grid view" ) : isGridLayout ? (
}
>
{isGridLayout ? (
<List className="h-4 w-4" />
) : (
<LayoutGrid className="h-4 w-4" /> <LayoutGrid className="h-4 w-4" />
) : (
<List className="h-4 w-4" />
)} )}
</Button> </Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
<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" /> {t('Applications.Views.GridView')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
<Grid3X3 className="h-4 w-4 mr-2" /> {t('Applications.Views.CompactView')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Select
value={String(itemsPerPage)}
onValueChange={handleItemsPerPageChange}
onOpenChange={(open) => {
if (open && customInputRef.current) {
customInputRef.current.value = String(itemsPerPage);
}
}}
>
<SelectTrigger className="w-[140px]">
<SelectValue>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
<SelectItem value={String(itemsPerPage)}>
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
</SelectItem>
) : null}
<SelectItem value="5">5 items</SelectItem>
<SelectItem value="10">10 items</SelectItem>
<SelectItem value="15">15 items</SelectItem>
<SelectItem value="20">20 items</SelectItem>
<SelectItem value="25">25 items</SelectItem>
<div className="p-2 border-t mt-1">
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
<div className="flex items-center gap-2 mt-1">
<Input
id="custom-items"
ref={customInputRef}
type="number"
min="1"
max="100"
className="h-8"
defaultValue={itemsPerPage}
onChange={(e) => {
// Don't immediately apply the change while typing
// Just validate the input for visual feedback
const value = parseInt(e.target.value);
if (isNaN(value) || value < 1 || value > 100) {
e.target.classList.add("border-red-500");
} else {
e.target.classList.remove("border-red-500");
}
}}
onBlur={(e) => {
// Apply the change when the input loses focus
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// Clear any existing debounce timer to apply immediately
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
// Apply change immediately on Enter
const validatedValue = Math.min(Math.max(value, 1), 100);
setItemsPerPage(validatedValue);
setCurrentPage(1);
Cookies.set("itemsPerPage-app", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
// Close the dropdown
document.body.click();
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
</div>
</div>
</SelectContent>
</Select>
{servers.length === 0 ? ( {servers.length === 0 ? (
<p className="text-muted-foreground"> <p className="text-muted-foreground">
You must first add a server. {t('Applications.Messages.AddServerFirst')}
</p> </p>
) : ( ) : (
<AlertDialog> <AlertDialog>
@@ -319,26 +497,26 @@ export default function Dashboard() {
<Plus /> <Plus />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Add an application</AlertDialogTitle> <AlertDialogTitle>{t('Applications.Add.Title')}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<div className="space-y-4 pt-4"> <div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label>Name</Label> <Label>{t('Applications.Add.Name')}</Label>
<Input <Input
placeholder="e.g. Portainer" placeholder={t('Applications.Add.NamePlaceholder')}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label>Server</Label> <Label>{t('Applications.Add.Server')}</Label>
<Select <Select
onValueChange={(v) => setServerId(Number(v))} onValueChange={(v) => setServerId(Number(v))}
required required
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Select server" /> <SelectValue placeholder={t('Applications.Add.SelectServer')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{servers.map((server) => ( {servers.map((server) => (
@@ -354,66 +532,84 @@ export default function Dashboard() {
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label> <Label>
Description{" "} {t('Applications.Add.Description')}{" "}
<span className="text-stone-600">(optional)</span> <span className="text-stone-600">{t('Common.optional')}</span>
</Label> </Label>
<Textarea <Textarea
placeholder="Application description" placeholder={t('Applications.Add.DescriptionPlaceholder')}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label> <Label>{t('Applications.Add.IconURL')}</Label>
Icon URL{" "}
<span className="text-stone-600">(optional)</span>
</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
value={icon} placeholder={t('Applications.Add.IconURLPlaceholder')}
placeholder="https://example.com/icon.png"
onChange={(e) => setIcon(e.target.value)} onChange={(e) => setIcon(e.target.value)}
value={icon}
/> />
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button variant="outline" size="icon" onClick={generateIconURL}> <Button variant="outline" size="icon" onClick={generateIconURL}>
<Zap /> <Zap />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
Generate Icon URL
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label>Public URL</Label> <Label>{t('Applications.Add.PublicURL')}</Label>
<Input <Input
placeholder="https://example.com" placeholder={t('Applications.Add.PublicURLPlaceholder')}
onChange={(e) => setPublicURL(e.target.value)} onChange={(e) => setPublicURL(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label> <Label>
Local URL{" "} {t('Applications.Add.LocalURL')}{" "}
<span className="text-stone-600">(optional)</span> <span className="text-stone-600">{t('Common.optional')}</span>
</Label> </Label>
<Input <Input
placeholder="http://localhost:3000" placeholder={t('Applications.Add.LocalURLPlaceholder')}
onChange={(e) => setLocalURL(e.target.value)} onChange={(e) => setLocalURL(e.target.value)}
/> />
</div> </div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="custom-uptime-check"
checked={customUptimeCheck}
onChange={(e) => setCustomUptimeCheck(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
<Label htmlFor="custom-uptime-check">{t('Applications.Add.CustomUptimeCheck')}</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
{t('Applications.Add.CustomUptimeCheckTooltip')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{customUptimeCheck && (
<div className="grid w-full items-center gap-1.5">
<Label>{t('Applications.Add.UptimeCheckURL')}</Label>
<Input
placeholder={t('Applications.Add.UptimeCheckURLPlaceholder')}
value={uptimecheckUrl}
onChange={(e) => setUptimecheckUrl(e.target.value)}
/>
</div>
)}
</div> </div>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={add} onClick={add}
disabled={!name || !publicURL || !serverId} disabled={!name || !publicURL || !serverId}
> >
Add {t('Common.add')}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -424,7 +620,7 @@ export default function Dashboard() {
<div className="flex flex-col gap-2 mb-4 pt-2"> <div className="flex flex-col gap-2 mb-4 pt-2">
<Input <Input
id="application-search" id="application-search"
placeholder="Type to search..." placeholder={t('Applications.Search.Placeholder')}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
@@ -433,12 +629,40 @@ export default function Dashboard() {
{!loading ? ( {!loading ? (
<div <div
className={ className={
isGridLayout isCompactLayout
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2"
: isGridLayout
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
: "space-y-4" : "space-y-4"
} }
> >
{applications.map((app) => ( {applications.map((app) => (
isCompactLayout ? (
<div
key={app.id}
className="bg-card rounded-md border p-3 flex flex-col items-center justify-between h-[120px] w-full cursor-pointer hover:shadow-md transition-shadow relative"
onClick={() => window.open(app.publicURL, "_blank")}
title={app.name}
>
<div className="absolute top-1 right-1">
<StatusIndicator isOnline={app.online} showLabel={false} />
</div>
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
{app.icon ? (
<img
src={app.icon}
alt={app.name}
className="w-full h-full object-contain rounded-md"
/>
) : (
<span className="text-gray-500 text-xs">{t('Applications.Card.Icon')}</span>
)}
</div>
<div className="text-center mt-2">
<h3 className="text-sm font-medium truncate w-full max-w-[110px]">{app.name}</h3>
</div>
</div>
) : (
<Card <Card
key={app.id} key={app.id}
className={ className={
@@ -451,7 +675,7 @@ export default function Dashboard() {
<div className="absolute top-2 right-2"> <div className="absolute top-2 right-2">
<StatusIndicator isOnline={app.online} /> <StatusIndicator isOnline={app.online} />
</div> </div>
<div className="flex items-center justify-between w-full mt-4 mb-4"> <div className={`flex ${isGridLayout ? 'flex-col' : 'items-center justify-between'} w-full mt-4 mb-4`}>
<div className="flex items-center"> <div className="flex items-center">
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md"> <div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
{app.icon ? ( {app.icon ? (
@@ -461,7 +685,7 @@ export default function Dashboard() {
className="w-full h-full object-contain rounded-md" 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>
<div className="ml-4"> <div className="ml-4">
@@ -473,10 +697,12 @@ export default function Dashboard() {
{app.description && ( {app.description && (
<br className="hidden md:block" /> <br className="hidden md:block" />
)} )}
Server: {app.server || "No server"} {t('Applications.Card.Server')}: {app.server || t('Applications.Card.NoServer')}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
{!isGridLayout && (
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]"> <div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<div className="flex flex-col space-y-2 flex-grow"> <div className="flex flex-col space-y-2 flex-grow">
@@ -488,7 +714,7 @@ export default function Dashboard() {
} }
> >
<Link className="h-4 w-4" /> <Link className="h-4 w-4" />
Public URL {t('Applications.Card.PublicURL')}
</Button> </Button>
{app.localURL && ( {app.localURL && (
<Button <Button
@@ -499,7 +725,7 @@ export default function Dashboard() {
} }
> >
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
Local URL {t('Applications.Card.LocalURL')}
</Button> </Button>
)} )}
</div> </div>
@@ -522,7 +748,7 @@ export default function Dashboard() {
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
Edit Application Edit Application
@@ -627,6 +853,36 @@ export default function Dashboard() {
} }
/> />
</div> </div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="edit-custom-uptime-check"
checked={editCustomUptimeCheck}
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
When enabled, this URL replaces the Public URL for uptime monitoring checks
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{editCustomUptimeCheck && (
<div className="grid w-full items-center gap-1.5">
<Label>Uptime Check URL</Label>
<Input
placeholder="https://example.com/status"
value={editUptimecheckUrl}
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
/>
</div>
)}
</div> </div>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@@ -646,16 +902,216 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
</CardHeader> </CardHeader>
{isGridLayout && (
<CardFooter className="mt-auto">
<div className="flex items-center gap-2 w-full">
<div className={`grid ${app.localURL ? 'grid-cols-2' : 'grid-cols-1'} gap-2 flex-grow`}>
<Button
variant="outline"
className="gap-2 w-full"
onClick={() => window.open(app.publicURL, "_blank")}
>
<Link className="h-4 w-4" />
{t('Applications.Card.PublicURL')}
</Button>
{app.localURL && (
<Button
variant="outline"
className="gap-2 w-full"
onClick={() => window.open(app.localURL, "_blank")}
>
<Home className="h-4 w-4" />
{t('Applications.Card.LocalURL')}
</Button>
)}
</div>
<div className="flex gap-2">
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() => deleteApplication(app.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="icon"
className="h-9 w-9"
onClick={() => openEditDialog(app)}
>
<Pencil className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
<AlertDialogHeader>
<AlertDialogTitle>
Edit Application
</AlertDialogTitle>
<AlertDialogDescription>
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label>Name</Label>
<Input
placeholder="e.g. Portainer"
value={editName}
onChange={(e) =>
setEditName(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>Server</Label>
<Select
value={
editServerId !== null
? String(editServerId)
: undefined
}
onValueChange={(v) =>
setEditServerId(Number(v))
}
required
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server" />
</SelectTrigger>
<SelectContent>
{servers.map((server) => (
<SelectItem
key={server.id}
value={String(server.id)}
>
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Description{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Textarea
placeholder="Application description"
value={editDescription}
onChange={(e) =>
setEditDescription(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Icon URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<div className="flex gap-2">
<Input
placeholder="https://example.com/icon.png"
value={editIcon}
onChange={(e) =>
setEditIcon(e.target.value)
}
/>
<Button variant="outline" size="icon" onClick={generateEditIconURL}>
<Zap />
</Button>
</div>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>Public URL</Label>
<Input
placeholder="https://example.com"
value={editPublicURL}
onChange={(e) =>
setEditPublicURL(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Local URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
placeholder="http://localhost:3000"
value={editLocalURL}
onChange={(e) =>
setEditLocalURL(e.target.value)
}
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="edit-custom-uptime-check"
checked={editCustomUptimeCheck}
onChange={(e) => setEditCustomUptimeCheck(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
<Label htmlFor="edit-custom-uptime-check">Custom Uptime Check URL</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
When enabled, this URL replaces the Public URL for uptime monitoring checks
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{editCustomUptimeCheck && (
<div className="grid w-full items-center gap-1.5">
<Label>Uptime Check URL</Label>
<Input
placeholder="https://example.com/status"
value={editUptimecheckUrl}
onChange={(e) => setEditUptimecheckUrl(e.target.value)}
/>
</div>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={edit}
disabled={
!editName || !editPublicURL || !editServerId
}
>
Save Changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</CardFooter>
)}
</Card> </Card>
)
))} ))}
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<div className="inline-block" role="status" aria-label="loading"> <div className="inline-block" role="status" aria-label="loading">
<svg <svg
className="w-6 h-6 stroke-white animate-spin " className="w-6 h-6 stroke-white animate-spin"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -675,11 +1131,18 @@ export default function Dashboard() {
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
<span className="sr-only">Loading...</span> <span className="sr-only">{t('Common.Loading')}</span>
</div> </div>
</div> </div>
)} )}
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{totalItems > 0
? t('Applications.Pagination.Showing', { start: startItem, end: endItem, total: totalItems })
: t('Applications.Pagination.NoApplications')}
</div>
</div>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>

View File

@@ -16,8 +16,10 @@ import {
import { ReactFlow, Controls, Background, ConnectionLineType } from "@xyflow/react"; import { ReactFlow, Controls, Background, ConnectionLineType } from "@xyflow/react";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
export default function Dashboard() { export default function Dashboard() {
const t = useTranslations();
const [nodes, setNodes] = useState<any[]>([]); const [nodes, setNodes] = useState<any[]>([]);
const [edges, setEdges] = useState<any[]>([]); const [edges, setEdges] = useState<any[]>([]);
@@ -58,13 +60,13 @@ export default function Dashboard() {
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" /> <BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage className="dark:text-slate-300"> <BreadcrumbPage className="dark:text-slate-300">
My Infrastructure {t('Network.Breadcrumb.MyInfrastructure')}
</BreadcrumbPage> </BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" /> <BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage className="dark:text-slate-300"> <BreadcrumbPage className="dark:text-slate-300">
Network {t('Network.Breadcrumb.Network')}
</BreadcrumbPage> </BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import NextLink from "next/link" import NextLink from "next/link"
import { useTranslations } from "next-intl"
interface ServerHistory { interface ServerHistory {
labels: string[]; labels: string[];
@@ -30,6 +31,8 @@ interface ServerHistory {
ram: (number | null)[]; ram: (number | null)[];
disk: (number | null)[]; disk: (number | null)[];
online: (boolean | null)[]; online: (boolean | null)[];
gpu: (number | null)[];
temp: (number | null)[];
} }
} }
@@ -54,8 +57,11 @@ interface Server {
cpuUsage: number; cpuUsage: number;
ramUsage: number; ramUsage: number;
diskUsage: number; diskUsage: number;
gpuUsage: number;
temp: number;
history?: ServerHistory; history?: ServerHistory;
port: number; port: number;
uptime?: string;
} }
interface GetServersResponse { interface GetServersResponse {
@@ -64,6 +70,7 @@ interface GetServersResponse {
} }
export default function ServerDetail() { export default function ServerDetail() {
const t = useTranslations()
const params = useParams() const params = useParams()
const serverId = params.server_id as string const serverId = params.server_id as string
const [server, setServer] = useState<Server | null>(null) const [server, setServer] = useState<Server | null>(null)
@@ -74,6 +81,8 @@ export default function ServerDetail() {
const cpuChartRef = { current: null as Chart | null } const cpuChartRef = { current: null as Chart | null }
const ramChartRef = { current: null as Chart | null } const ramChartRef = { current: null as Chart | null }
const diskChartRef = { 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 () => { const fetchServerDetails = async () => {
try { try {
@@ -104,6 +113,8 @@ export default function ServerDetail() {
if (cpuChartRef.current) cpuChartRef.current.destroy(); if (cpuChartRef.current) cpuChartRef.current.destroy();
if (ramChartRef.current) ramChartRef.current.destroy(); if (ramChartRef.current) ramChartRef.current.destroy();
if (diskChartRef.current) diskChartRef.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 // Wait for DOM to be ready
const initTimer = setTimeout(() => { const initTimer = setTimeout(() => {
@@ -205,7 +216,7 @@ export default function ServerDetail() {
data: { data: {
labels: timeLabels, labels: timeLabels,
datasets: [{ datasets: [{
label: 'CPU Usage', label: t('Common.Server.CPU') + ' ' + t('Common.Server.Usage'),
data: history.datasets.cpu, data: history.datasets.cpu,
borderColor: 'rgb(75, 192, 192)', borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)', backgroundColor: 'rgba(75, 192, 192, 0.1)',
@@ -218,7 +229,7 @@ export default function ServerDetail() {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'CPU Usage History', text: t('Common.Server.CPU') + ' ' + t('Server.UsageHistory'),
font: { font: {
size: 14 size: 14
} }
@@ -252,7 +263,7 @@ export default function ServerDetail() {
data: { data: {
labels: timeLabels, labels: timeLabels,
datasets: [{ datasets: [{
label: 'RAM Usage', label: t('Common.Server.RAM') + ' ' + t('Common.Server.Usage'),
data: history.datasets.ram, data: history.datasets.ram,
borderColor: 'rgb(153, 102, 255)', borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)', backgroundColor: 'rgba(153, 102, 255, 0.1)',
@@ -265,7 +276,7 @@ export default function ServerDetail() {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'RAM Usage History', text: t('Common.Server.RAM') + ' ' + t('Server.UsageHistory'),
font: { font: {
size: 14 size: 14
} }
@@ -299,7 +310,7 @@ export default function ServerDetail() {
data: { data: {
labels: timeLabels, labels: timeLabels,
datasets: [{ datasets: [{
label: 'Disk Usage', label: t('Common.Server.Disk') + ' ' + t('Common.Server.Usage'),
data: history.datasets.disk, data: history.datasets.disk,
borderColor: 'rgb(255, 159, 64)', borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.1)', backgroundColor: 'rgba(255, 159, 64, 0.1)',
@@ -312,7 +323,7 @@ export default function ServerDetail() {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'Disk Usage History', text: t('Common.Server.Disk') + ' ' + t('Server.UsageHistory'),
font: { font: {
size: 14 size: 14
} }
@@ -338,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); }, 100);
return () => { return () => {
@@ -345,6 +455,8 @@ export default function ServerDetail() {
if (cpuChartRef.current) cpuChartRef.current.destroy(); if (cpuChartRef.current) cpuChartRef.current.destroy();
if (ramChartRef.current) ramChartRef.current.destroy(); if (ramChartRef.current) ramChartRef.current.destroy();
if (diskChartRef.current) diskChartRef.current.destroy(); if (diskChartRef.current) diskChartRef.current.destroy();
if (gpuChartRef.current) gpuChartRef.current.destroy();
if (tempChartRef.current) tempChartRef.current.destroy();
}; };
}, [server, timeRange]); }, [server, timeRange]);
@@ -368,12 +480,12 @@ export default function ServerDetail() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>My Infrastructure</BreadcrumbPage> <BreadcrumbPage>{t('Servers.MyInfrastructure')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<NextLink href="/dashboard/servers" className="hover:underline"> <NextLink href="/dashboard/servers" className="hover:underline">
<BreadcrumbPage>Servers</BreadcrumbPage> <BreadcrumbPage>{t('Servers.Title')}</BreadcrumbPage>
</NextLink> </NextLink>
</BreadcrumbItem> </BreadcrumbItem>
{server && ( {server && (
@@ -414,7 +526,7 @@ export default function ServerDetail() {
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
<span className="sr-only">Loading...</span> <span className="sr-only">{t('Common.Loading')}</span>
</div> </div>
</div> </div>
) : server ? ( ) : server ? (
@@ -430,42 +542,47 @@ export default function ServerDetail() {
{server.name} {server.name}
</CardTitle> </CardTitle>
<CardDescription> <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 && ( {server.isVM && server.hostServer && (
<> Hosted on {server.hostedVMs?.[0]?.name}</> <> {t('Server.HostedOn')} {server.hostedVMs?.[0]?.name}</>
)} )}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</div> </div>
{server.monitoring && ( {server.monitoring && (
<div className="absolute top-0 right-4"> <div className="absolute top-0 right-4 flex flex-col items-end">
<StatusIndicator isOnline={server.online} /> <StatusIndicator isOnline={server.online} />
{server.online && server.uptime && (
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
{t('Common.since', { date: server.uptime })}
</span>
)}
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2"> <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="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>{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>{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>{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>{server.disk || "-"}</div>
</div> </div>
</div> </div>
<div className="space-y-2"> <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="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>{server.ip || "-"}</div>
<div className="text-muted-foreground">Management URL:</div> <div className="text-muted-foreground">{t('Server.ManagementURL')}:</div>
<div> <div>
{server.url ? ( {server.url ? (
<a href={server.url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 text-blue-500 hover:underline"> <a href={server.url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 text-blue-500 hover:underline">
@@ -480,9 +597,9 @@ export default function ServerDetail() {
{server.monitoring && ( {server.monitoring && (
<div className="space-y-2"> <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="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="flex items-center gap-2">
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden"> <div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
<div <div
@@ -490,9 +607,9 @@ export default function ServerDetail() {
style={{ width: `${server.cpuUsage}%` }} style={{ width: `${server.cpuUsage}%` }}
/> />
</div> </div>
<span>{server.cpuUsage}%</span> <span>{server.cpuUsage !== null && server.cpuUsage !== undefined ? `${server.cpuUsage}%` : t('Common.noData')}</span>
</div> </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="flex items-center gap-2">
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden"> <div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
<div <div
@@ -500,9 +617,9 @@ export default function ServerDetail() {
style={{ width: `${server.ramUsage}%` }} style={{ width: `${server.ramUsage}%` }}
/> />
</div> </div>
<span>{server.ramUsage}%</span> <span>{server.ramUsage !== null && server.ramUsage !== undefined ? `${server.ramUsage}%` : t('Common.noData')}</span>
</div> </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="flex items-center gap-2">
<div className="w-full h-2 bg-secondary rounded-full overflow-hidden"> <div className="w-full h-2 bg-secondary rounded-full overflow-hidden">
<div <div
@@ -510,8 +627,36 @@ export default function ServerDetail() {
style={{ width: `${server.diskUsage}%` }} style={{ width: `${server.diskUsage}%` }}
/> />
</div> </div>
<span>{server.diskUsage}%</span> <span>{server.diskUsage !== null && server.diskUsage !== undefined ? `${server.diskUsage}%` : t('Common.noData')}</span>
</div> </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>
</div> </div>
)} )}
@@ -526,30 +671,30 @@ export default function ServerDetail() {
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle>Resource Usage History</CardTitle> <CardTitle>{t('Server.ResourceUsageHistory')}</CardTitle>
<CardDescription> <CardDescription>
{timeRange === '1h' {timeRange === '1h'
? 'Last hour, per minute' ? t('Server.TimeRange.LastHour')
: timeRange === '1d' : timeRange === '1d'
? 'Last 24 hours, 15-minute intervals' ? t('Server.TimeRange.Last24Hours')
: timeRange === '7d' : timeRange === '7d'
? 'Last 7 days, hourly intervals' ? t('Server.TimeRange.Last7Days')
: 'Last 30 days, 4-hour intervals'} : t('Server.TimeRange.Last30Days')}
</CardDescription> </CardDescription>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={timeRange} onValueChange={(value: '1h' | '1d' | '7d' | '30d') => setTimeRange(value)}> <Select value={timeRange} onValueChange={(value: '1h' | '1d' | '7d' | '30d') => setTimeRange(value)}>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Time range" /> <SelectValue placeholder={t('Server.TimeRange.Select')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="1h">Last Hour</SelectItem> <SelectItem value="1h">{t('Server.TimeRange.LastHour')}</SelectItem>
<SelectItem value="1d">Last 24 Hours</SelectItem> <SelectItem value="1d">{t('Server.TimeRange.Last24Hours')}</SelectItem>
<SelectItem value="7d">Last 7 Days</SelectItem> <SelectItem value="7d">{t('Server.TimeRange.Last7Days')}</SelectItem>
<SelectItem value="30d">Last 30 Days</SelectItem> <SelectItem value="30d">{t('Server.TimeRange.Last30Days')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button variant="outline" onClick={refreshData}>Refresh</Button> <Button variant="outline" onClick={refreshData}>{t('Common.Refresh')}</Button>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -564,6 +709,16 @@ export default function ServerDetail() {
<div className="h-[200px] relative bg-background"> <div className="h-[200px] relative bg-background">
<canvas id="disk-chart" /> <canvas id="disk-chart" />
</div> </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> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -574,8 +729,8 @@ export default function ServerDetail() {
{server.hostedVMs && server.hostedVMs.length > 0 && ( {server.hostedVMs && server.hostedVMs.length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Virtual Machines</CardTitle> <CardTitle>{t('Server.VirtualMachines')}</CardTitle>
<CardDescription>Virtual machines hosted on this server</CardDescription> <CardDescription>{t('Server.VirtualMachinesDescription')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -600,7 +755,14 @@ export default function ServerDetail() {
</NextLink> </NextLink>
</div> </div>
{hostedVM.monitoring && ( {hostedVM.monitoring && (
<div className="flex flex-col items-end">
<StatusIndicator isOnline={hostedVM.online} /> <StatusIndicator isOnline={hostedVM.online} />
{hostedVM.online && hostedVM.uptime && (
<span className="text-xs text-muted-foreground mt-1 w-max text-right whitespace-nowrap">
{t('Common.since', { date: hostedVM.uptime })}
</span>
)}
</div>
)} )}
</div> </div>
@@ -612,43 +774,43 @@ export default function ServerDetail() {
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<MonitorCog className="h-4 w-4 text-muted-foreground" /> <MonitorCog className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>OS:</b> {hostedVM.os || "-"} <b>{t('Common.Server.OS')}:</b> {hostedVM.os || "-"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" /> <FileDigit className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>IP:</b> {hostedVM.ip || "Not set"} <b>{t('Common.Server.IP')}:</b> {hostedVM.ip || t('Common.notSet')}
</span> </span>
</div> </div>
</div> </div>
<div className="col-span-full mb-2"> <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>
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" /> <Cpu className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>CPU:</b> {hostedVM.cpu || "-"} <b>{t('Common.Server.CPU')}:</b> {hostedVM.cpu || "-"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<Microchip className="h-4 w-4 text-muted-foreground" /> <Microchip className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>GPU:</b> {hostedVM.gpu || "-"} <b>{t('Common.Server.GPU')}:</b> {hostedVM.gpu || "-"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<MemoryStick className="h-4 w-4 text-muted-foreground" /> <MemoryStick className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>RAM:</b> {hostedVM.ram || "-"} <b>{t('Common.Server.RAM')}:</b> {hostedVM.ram || "-"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-foreground/80"> <div className="flex items-center gap-2 text-foreground/80">
<HardDrive className="h-4 w-4 text-muted-foreground" /> <HardDrive className="h-4 w-4 text-muted-foreground" />
<span> <span>
<b>Disk:</b> {hostedVM.disk || "-"} <b>{t('Common.Server.Disk')}:</b> {hostedVM.disk || "-"}
</span> </span>
</div> </div>
@@ -663,7 +825,7 @@ export default function ServerDetail() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" /> <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> </div>
<span className="text-xs font-medium"> <span className="text-xs font-medium">
{hostedVM.cpuUsage || 0}% {hostedVM.cpuUsage || 0}%
@@ -681,7 +843,7 @@ export default function ServerDetail() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" /> <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> </div>
<span className="text-xs font-medium"> <span className="text-xs font-medium">
{hostedVM.ramUsage || 0}% {hostedVM.ramUsage || 0}%
@@ -699,7 +861,7 @@ export default function ServerDetail() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" /> <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> </div>
<span className="text-xs font-medium"> <span className="text-xs font-medium">
{hostedVM.diskUsage || 0}% {hostedVM.diskUsage || 0}%
@@ -724,8 +886,8 @@ export default function ServerDetail() {
</div> </div>
) : ( ) : (
<div className="text-center p-12"> <div className="text-center p-12">
<h2 className="text-2xl font-bold">Server not found</h2> <h2 className="text-2xl font-bold">{t('Server.NotFound')}</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> <p className="text-muted-foreground mt-2">{t('Server.NotFoundDescription')}</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -19,9 +19,10 @@ import axios from "axios"
import Cookies from "js-cookie" import Cookies from "js-cookie"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle, Check, Palette, User, 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 { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner" import { toast } from "sonner"
import { Textarea } from "@/components/ui/textarea"
import { import {
AlertDialog, AlertDialog,
@@ -35,7 +36,7 @@ import {
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Textarea } from "@/components/ui/textarea" import { useTranslations } from "next-intl"
interface NotificationsResponse { interface NotificationsResponse {
notifications: any[] notifications: any[]
@@ -46,6 +47,7 @@ interface NotificationResponse {
} }
export default function Settings() { export default function Settings() {
const t = useTranslations()
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
@@ -62,6 +64,7 @@ export default function Settings() {
const [emailSuccess, setEmailSuccess] = useState<boolean>(false) const [emailSuccess, setEmailSuccess] = useState<boolean>(false)
const [notificationType, setNotificationType] = useState<string>("") const [notificationType, setNotificationType] = useState<string>("")
const [notificationName, setNotificationName] = useState<string>("")
const [smtpHost, setSmtpHost] = useState<string>("") const [smtpHost, setSmtpHost] = useState<string>("")
const [smtpPort, setSmtpPort] = useState<number>(0) const [smtpPort, setSmtpPort] = useState<number>(0)
const [smtpSecure, setSmtpSecure] = useState<boolean>(false) const [smtpSecure, setSmtpSecure] = useState<boolean>(false)
@@ -79,7 +82,8 @@ export default function Settings() {
const [pushoverUrl, setPushoverUrl] = useState<string>("") const [pushoverUrl, setPushoverUrl] = useState<string>("")
const [pushoverToken, setPushoverToken] = useState<string>("") const [pushoverToken, setPushoverToken] = useState<string>("")
const [pushoverUser, setPushoverUser] = useState<string>("") const [pushoverUser, setPushoverUser] = useState<string>("")
const [echobellURL, setEchobellURL] = useState<string>("")
const [language, setLanguage] = useState<string>("english")
const [notifications, setNotifications] = useState<any[]>([]) const [notifications, setNotifications] = useState<any[]>([])
const [notificationTextApplication, setNotificationTextApplication] = useState<string>("") const [notificationTextApplication, setNotificationTextApplication] = useState<string>("")
@@ -91,7 +95,7 @@ export default function Settings() {
setEmailError("") setEmailError("")
if (!email) { if (!email) {
setEmailError("Email is required") setEmailError(t('Settings.UserSettings.ChangeEmail.EmailRequired'))
setEmailErrorVisible(true) setEmailErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setEmailErrorVisible(false) setEmailErrorVisible(false)
@@ -122,7 +126,7 @@ export default function Settings() {
const changePassword = async () => { const changePassword = async () => {
try { try {
if (password !== confirmPassword) { if (password !== confirmPassword) {
setPasswordError("Passwords do not match") setPasswordError(t('Settings.UserSettings.ChangePassword.PasswordsDontMatch'))
setPasswordErrorVisible(true) setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false) setPasswordErrorVisible(false)
@@ -131,7 +135,7 @@ export default function Settings() {
return return
} }
if (!oldPassword || !password || !confirmPassword) { if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required") setPasswordError(t('Settings.UserSettings.ChangePassword.AllFieldsRequired'))
setPasswordErrorVisible(true) setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false) setPasswordErrorVisible(false)
@@ -168,6 +172,7 @@ export default function Settings() {
const addNotification = async () => { const addNotification = async () => {
try { try {
const response = await axios.post("/api/notifications/add", { const response = await axios.post("/api/notifications/add", {
name: notificationName,
type: notificationType, type: notificationType,
smtpHost: smtpHost, smtpHost: smtpHost,
smtpPort: smtpPort, smtpPort: smtpPort,
@@ -186,6 +191,7 @@ export default function Settings() {
pushoverUrl: pushoverUrl, pushoverUrl: pushoverUrl,
pushoverToken: pushoverToken, pushoverToken: pushoverToken,
pushoverUser: pushoverUser, pushoverUser: pushoverUser,
echobellURL: echobellURL,
}) })
getNotifications() getNotifications()
} catch (error: any) { } catch (error: any) {
@@ -261,12 +267,32 @@ export default function Settings() {
const response = await axios.post("/api/notifications/test", { const response = await axios.post("/api/notifications/test", {
notificationId: id, notificationId: id,
}) })
toast.success("Notification will be sent in a few seconds.") toast.success(t('Settings.Notifications.TestSuccess'))
} catch (error: any) { } catch (error: any) {
toast.error(error.response.data.error) 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 ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
@@ -278,15 +304,11 @@ export default function Settings() {
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem className="hidden md:block"> <BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>/</BreadcrumbPage> <BreadcrumbPage>{t('Settings.Breadcrumb.Dashboard')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Dashboard</BreadcrumbPage> <BreadcrumbPage>{t('Settings.Breadcrumb.Settings')}</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Settings</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
@@ -294,31 +316,31 @@ export default function Settings() {
</header> </header>
<div className="p-6"> <div className="p-6">
<div className="pb-4"> <div className="pb-4">
<span className="text-3xl font-bold">Settings</span> <span className="text-3xl font-bold">{t('Settings.Title')}</span>
</div> </div>
<div className="grid gap-6"> <div className="grid gap-6">
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm"> <Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b"> <CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" /> <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> </div>
</CardHeader> </CardHeader>
<CardContent className="pb-6"> <CardContent className="pb-6">
<div className="text-sm text-muted-foreground mb-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>
<div className="grid md:grid-cols-2 gap-8"> <div className="grid md:grid-cols-2 gap-8">
<div className="space-y-4"> <div className="space-y-4">
<div className="border-b pb-2"> <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> </div>
{emailErrorVisible && ( {emailErrorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50"> <Alert variant="destructive" className="animate-in fade-in-50">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle> <AlertTitle>{t('Common.Error')}</AlertTitle>
<AlertDescription>{emailError}</AlertDescription> <AlertDescription>{emailError}</AlertDescription>
</Alert> </Alert>
)} )}
@@ -326,34 +348,33 @@ export default function Settings() {
{emailSuccess && ( {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"> <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" /> <Check className="h-4 w-4" />
<AlertTitle>Success</AlertTitle> <AlertTitle>{t('Settings.UserSettings.ChangeEmail.Success')}</AlertTitle>
<AlertDescription>Email changed successfully.</AlertDescription>
</Alert> </Alert>
)} )}
<div className="space-y-3"> <div className="space-y-3">
<Input <Input
type="email" type="email"
placeholder="Enter new email" placeholder={t('Settings.UserSettings.ChangeEmail.Placeholder')}
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="h-11" className="h-11"
/> />
<Button onClick={changeEmail} className="w-full h-11"> <Button onClick={changeEmail} className="w-full h-11">
Change Email {t('Settings.UserSettings.ChangeEmail.Button')}
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="border-b pb-2"> <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> </div>
{passwordErrorVisible && ( {passwordErrorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50"> <Alert variant="destructive" className="animate-in fade-in-50">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle> <AlertTitle>{t('Common.Error')}</AlertTitle>
<AlertDescription>{passwordError}</AlertDescription> <AlertDescription>{passwordError}</AlertDescription>
</Alert> </Alert>
)} )}
@@ -361,35 +382,34 @@ export default function Settings() {
{passwordSuccess && ( {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"> <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" /> <Check className="h-4 w-4" />
<AlertTitle>Success</AlertTitle> <AlertTitle>{t('Settings.UserSettings.ChangePassword.Success')}</AlertTitle>
<AlertDescription>Password changed successfully.</AlertDescription>
</Alert> </Alert>
)} )}
<div className="space-y-3"> <div className="space-y-3">
<Input <Input
type="password" type="password"
placeholder="Enter old password" placeholder={t('Settings.UserSettings.ChangePassword.OldPassword')}
value={oldPassword} value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)} onChange={(e) => setOldPassword(e.target.value)}
className="h-11" className="h-11"
/> />
<Input <Input
type="password" type="password"
placeholder="Enter new password" placeholder={t('Settings.UserSettings.ChangePassword.NewPassword')}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="h-11" className="h-11"
/> />
<Input <Input
type="password" type="password"
placeholder="Confirm new password" placeholder={t('Settings.UserSettings.ChangePassword.ConfirmPassword')}
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
className="h-11" className="h-11"
/> />
<Button onClick={changePassword} className="w-full h-11"> <Button onClick={changePassword} className="w-full h-11">
Change Password {t('Settings.UserSettings.ChangePassword.Button')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -401,25 +421,53 @@ export default function Settings() {
<CardHeader className="bg-muted/10 px-6 py-4 border-b"> <CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Palette className="h-5 w-5 text-primary" /> <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> </div>
</CardHeader> </CardHeader>
<CardContent className="pb-6"> <CardContent className="pb-6">
<div className="text-sm text-muted-foreground mb-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>
<div className="max-w-md"> <div className="max-w-md">
<Select value={theme} onValueChange={(value: string) => setTheme(value)}> <Select value={theme} onValueChange={(value: string) => setTheme(value)}>
<SelectTrigger className="w-full h-11"> <SelectTrigger className="w-full h-11">
<SelectValue> <SelectValue>
{(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)} {t(`Settings.ThemeSettings.${(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}`)}
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="light">Light</SelectItem> <SelectItem value="light">{t('Settings.ThemeSettings.Light')}</SelectItem>
<SelectItem value="dark">Dark</SelectItem> <SelectItem value="dark">{t('Settings.ThemeSettings.Dark')}</SelectItem>
<SelectItem value="system">System</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> </SelectContent>
</Select> </Select>
</div> </div>
@@ -432,54 +480,60 @@ export default function Settings() {
<div className="bg-muted/20 p-2 rounded-full"> <div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" /> <Bell className="h-5 w-5 text-primary" />
</div> </div>
<h2 className="text-xl font-semibold">Notifications</h2> <h2 className="text-xl font-semibold">{t('Settings.Notifications.Title')}</h2>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="text-sm text-muted-foreground mb-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>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button className="w-full h-11 flex items-center gap-2"> <Button className="w-full h-11 flex items-center gap-2">
Add Notification Channel {t('Settings.Notifications.AddChannel')}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle>Add Notification</AlertDialogTitle> <AlertDialogTitle>{t('Settings.Notifications.AddNotification.Title')}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<div className="space-y-4">
<Input
type="text"
id="notificationName"
placeholder={t('Settings.Notifications.AddNotification.Name')}
onChange={(e) => setNotificationName(e.target.value)}
/>
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}> <Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Notification Type" /> <SelectValue placeholder={t('Settings.Notifications.AddNotification.Type')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="smtp">SMTP</SelectItem> <SelectItem value="smtp">{t('Settings.Notifications.AddNotification.SMTP.Title')}</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem> <SelectItem value="telegram">{t('Settings.Notifications.AddNotification.Telegram.Title')}</SelectItem>
<SelectItem value="discord">Discord</SelectItem> <SelectItem value="discord">{t('Settings.Notifications.AddNotification.Discord.Title')}</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem> <SelectItem value="gotify">{t('Settings.Notifications.AddNotification.Gotify.Title')}</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem> <SelectItem value="ntfy">{t('Settings.Notifications.AddNotification.Ntfy.Title')}</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem> <SelectItem value="pushover">{t('Settings.Notifications.AddNotification.Pushover.Title')}</SelectItem>
<SelectItem value="echobell">{t('Settings.Notifications.AddNotification.Echobell.Title')}</SelectItem>
</SelectContent> </SelectContent>
{notificationType === "smtp" && ( {notificationType === "smtp" && (
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpHost">SMTP Host</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.Host')}</Label>
<Input <Input
type="text" type="text"
id="smtpHost"
placeholder="smtp.example.com" placeholder="smtp.example.com"
onChange={(e) => setSmtpHost(e.target.value)} onChange={(e) => setSmtpHost(e.target.value)}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpPort">SMTP Port</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.Port')}</Label>
<Input <Input
type="number" type="number"
id="smtpPort"
placeholder="587" placeholder="587"
onChange={(e) => setSmtpPort(Number(e.target.value))} onChange={(e) => setSmtpPort(Number(e.target.value))}
/> />
@@ -489,26 +543,24 @@ export default function Settings() {
<div className="flex items-center space-x-2 pt-2 pb-4"> <div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} /> <Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none"> <Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL) {t('Settings.Notifications.AddNotification.SMTP.Secure')}
</Label> </Label>
</div> </div>
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpUser">SMTP Username</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.Username')}</Label>
<Input <Input
type="text" type="text"
id="smtpUser"
placeholder="user@example.com" placeholder="user@example.com"
onChange={(e) => setSmtpUsername(e.target.value)} onChange={(e) => setSmtpUsername(e.target.value)}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpPass">SMTP Password</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.Password')}</Label>
<Input <Input
type="password" type="password"
id="smtpPass"
placeholder="••••••••" placeholder="••••••••"
onChange={(e) => setSmtpPassword(e.target.value)} onChange={(e) => setSmtpPassword(e.target.value)}
/> />
@@ -516,20 +568,18 @@ export default function Settings() {
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpFrom">From Address</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.From')}</Label>
<Input <Input
type="email" type="email"
id="smtpFrom"
placeholder="noreply@example.com" placeholder="noreply@example.com"
onChange={(e) => setSmtpFrom(e.target.value)} onChange={(e) => setSmtpFrom(e.target.value)}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="smtpTo">To Address</Label> <Label>{t('Settings.Notifications.AddNotification.SMTP.To')}</Label>
<Input <Input
type="email" type="email"
id="smtpTo"
placeholder="admin@example.com" placeholder="admin@example.com"
onChange={(e) => setSmtpTo(e.target.value)} onChange={(e) => setSmtpTo(e.target.value)}
/> />
@@ -542,20 +592,16 @@ export default function Settings() {
{notificationType === "telegram" && ( {notificationType === "telegram" && (
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="telegramToken"
placeholder=""
onChange={(e) => setTelegramToken(e.target.value)} onChange={(e) => setTelegramToken(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="telegramChatId"
placeholder=""
onChange={(e) => setTelegramChatId(e.target.value)} onChange={(e) => setTelegramChatId(e.target.value)}
/> />
</div> </div>
@@ -565,11 +611,9 @@ export default function Settings() {
{notificationType === "discord" && ( {notificationType === "discord" && (
<div className="mt-4"> <div className="mt-4">
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="discordWebhook"
placeholder=""
onChange={(e) => setDiscordWebhook(e.target.value)} onChange={(e) => setDiscordWebhook(e.target.value)}
/> />
</div> </div>
@@ -579,19 +623,15 @@ export default function Settings() {
{notificationType === "gotify" && ( {notificationType === "gotify" && (
<div className="mt-4"> <div className="mt-4">
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="gotifyUrl"
placeholder=""
onChange={(e) => setGotifyUrl(e.target.value)} onChange={(e) => setGotifyUrl(e.target.value)}
/> />
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="gotifyToken"
placeholder=""
onChange={(e) => setGotifyToken(e.target.value)} onChange={(e) => setGotifyToken(e.target.value)}
/> />
</div> </div>
@@ -602,19 +642,15 @@ export default function Settings() {
{notificationType === "ntfy" && ( {notificationType === "ntfy" && (
<div className="mt-4"> <div className="mt-4">
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="ntfyUrl"
placeholder=""
onChange={(e) => setNtfyUrl(e.target.value)} onChange={(e) => setNtfyUrl(e.target.value)}
/> />
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="ntfyToken"
placeholder=""
onChange={(e) => setNtfyToken(e.target.value)} onChange={(e) => setNtfyToken(e.target.value)}
/> />
</div> </div>
@@ -625,41 +661,51 @@ export default function Settings() {
{notificationType === "pushover" && ( {notificationType === "pushover" && (
<div className="mt-4 flex flex-col gap-2"> <div className="mt-4 flex flex-col gap-2">
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="pushoverUrl"
placeholder="e.g. https://api.pushover.net/1/messages.json"
onChange={(e) => setPushoverUrl(e.target.value)} onChange={(e) => setPushoverUrl(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="pushoverToken"
placeholder="e.g. 1234567890"
onChange={(e) => setPushoverToken(e.target.value)} onChange={(e) => setPushoverToken(e.target.value)}
/> />
</div> </div>
<div className="grid w-full items-center gap-1.5"> <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 <Input
type="text" type="text"
id="pushoverUser"
placeholder="e.g. 1234567890"
onChange={(e) => setPushoverUser(e.target.value)} onChange={(e) => setPushoverUser(e.target.value)}
/> />
</div> </div>
</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> </Select>
</div>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction> <AlertDialogAction onClick={addNotification}>{t('Common.add')}</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
@@ -667,65 +713,63 @@ export default function Settings() {
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button className="w-full h-11" variant="outline"> <Button className="w-full h-11" variant="outline">
Customize Notification Text {t('Settings.Notifications.CustomizeText.Display')}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle> <AlertDialogTitle>{t('Settings.Notifications.CustomizeText.Title')}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="text_application">Notification Text for Applications</Label> <Label>{t('Settings.Notifications.CustomizeText.Application')}</Label>
<Textarea <Textarea
id="text_application"
placeholder="Type here..."
value={notificationTextApplication} value={notificationTextApplication}
onChange={(e) => setNotificationTextApplication(e.target.value)} onChange={(e) => setNotificationTextApplication(e.target.value)}
rows={4} rows={4}
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="text_server">Notification Text for Servers</Label> <Label>{t('Settings.Notifications.CustomizeText.Server')}</Label>
<Textarea <Textarea
id="text_server"
placeholder="Type here..."
value={notificationTextServer} value={notificationTextServer}
onChange={(e) => setNotificationTextServer(e.target.value)} onChange={(e) => setNotificationTextServer(e.target.value)}
rows={4} rows={4}
/> />
</div> </div>
</div>
<div className="pt-4 text-sm text-muted-foreground"> <div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text: {t('Settings.Notifications.CustomizeText.Placeholders.Title')}
<ul className="list-disc list-inside space-y-1 pt-2"> <ul className="list-disc list-inside space-y-1 pt-2">
<li> <li>
<b>Server related:</b> <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"> <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>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Name')}</li>
<li>!status - The current status of the server (online/offline)</li> <li>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Status')}</li>
</ul> </ul>
</li> </li>
<li> <li>
<b>Application related:</b> <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"> <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>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Name')}</li>
<li>!url - The URL where the application is hosted</li> <li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Url')}</li>
<li>!status - The current status of the application (online/offline)</li> <li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Status')}</li>
</ul> </ul>
</li> </li>
</ul> </ul>
</div> </div>
</div>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction> <AlertDialogAction onClick={editNotificationText}>
{t('Common.Save')}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
<div className="mt-8"> <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"> <div className="space-y-3">
{notifications.length > 0 ? ( {notifications.length > 0 ? (
notifications.map((notification) => ( notifications.map((notification) => (
@@ -765,14 +809,12 @@ export default function Settings() {
</div> </div>
)} )}
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-medium capitalize">{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"> <p className="text-xs text-muted-foreground">
{notification.type === "smtp" && "Email notifications"} {t(`Settings.Notifications.AddNotification.${notification.type.charAt(0).toUpperCase() + notification.type.slice(1)}.Description`)}
{notification.type === "telegram" && "Telegram bot alerts"}
{notification.type === "discord" && "Discord webhook alerts"}
{notification.type === "gotify" && "Gotify notifications"}
{notification.type === "ntfy" && "Ntfy notifications"}
{notification.type === "pushover" && "Pushover notifications"}
</p> </p>
</div> </div>
</div> </div>
@@ -784,7 +826,7 @@ export default function Settings() {
onClick={() => testNotification(notification.id)} onClick={() => testNotification(notification.id)}
> >
<Play className="h-4 w-4 mr-1" /> <Play className="h-4 w-4 mr-1" />
Test {t('Settings.Notifications.Test')}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -793,6 +835,7 @@ export default function Settings() {
onClick={() => deleteNotification(notification.id)} onClick={() => deleteNotification(notification.id)}
> >
<Trash2 className="h-4 w-4 mr-1" /> <Trash2 className="h-4 w-4 mr-1" />
{t('Settings.Notifications.Delete')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -804,9 +847,11 @@ export default function Settings() {
<Bell className="h-6 w-6 text-muted-foreground" /> <Bell className="h-6 w-6 text-muted-foreground" />
</div> </div>
</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"> <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> </p>
</div> </div>
)} )}

View File

@@ -13,7 +13,6 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import { Card, CardHeader } from "@/components/ui/card"; import { Card, CardHeader } from "@/components/ui/card";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
@@ -26,6 +25,13 @@ import {
PaginationNext, PaginationNext,
PaginationLink, PaginationLink,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { useState, useEffect, useRef } from "react";
import Cookies from "js-cookie";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
import { useTranslations } from "next-intl";
const timeFormats = { const timeFormats = {
1: (timestamp: string) => 1: (timestamp: string) =>
@@ -76,6 +82,7 @@ interface PaginationData {
} }
export default function Uptime() { export default function Uptime() {
const t = useTranslations();
const [data, setData] = useState<UptimeData[]>([]); const [data, setData] = useState<UptimeData[]>([]);
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1); const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
const [pagination, setPagination] = useState<PaginationData>({ const [pagination, setPagination] = useState<PaginationData>({
@@ -85,7 +92,15 @@ export default function Uptime() {
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const getData = async (selectedTimespan: number, page: number) => { const savedItemsPerPage = Cookies.get("itemsPerPage-uptime");
const defaultItemsPerPage = 5;
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const getData = async (selectedTimespan: number, page: number, itemsPerPage: number) => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await axios.post<{ const response = await axios.post<{
@@ -93,7 +108,8 @@ export default function Uptime() {
pagination: PaginationData; pagination: PaginationData;
}>("/api/applications/uptime", { }>("/api/applications/uptime", {
timespan: selectedTimespan, timespan: selectedTimespan,
page page,
itemsPerPage
}); });
setData(response.data.data); setData(response.data.data);
@@ -114,17 +130,44 @@ export default function Uptime() {
const handlePrevious = () => { const handlePrevious = () => {
const newPage = Math.max(1, pagination.currentPage - 1); const newPage = Math.max(1, pagination.currentPage - 1);
setPagination(prev => ({...prev, currentPage: newPage})); setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage); getData(timespan, newPage, itemsPerPage);
}; };
const handleNext = () => { const handleNext = () => {
const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1); const newPage = Math.min(pagination.totalPages, pagination.currentPage + 1);
setPagination(prev => ({...prev, currentPage: newPage})); setPagination(prev => ({...prev, currentPage: newPage}));
getData(timespan, newPage); getData(timespan, newPage, itemsPerPage);
};
const handleItemsPerPageChange = (value: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
const newItemsPerPage = parseInt(value);
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
toast.error(t('Uptime.Messages.NumberValidation'));
return;
}
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
setItemsPerPage(validatedValue);
setPagination(prev => ({...prev, currentPage: 1}));
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
getData(timespan, 1, validatedValue);
}, 300);
}; };
useEffect(() => { useEffect(() => {
getData(timespan, 1); getData(timespan, 1, itemsPerPage);
}, [timespan]); }, [timespan]);
return ( return (
@@ -142,19 +185,100 @@ export default function Uptime() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>My Infrastructure</BreadcrumbPage> <BreadcrumbPage>{t('Uptime.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Uptime</BreadcrumbPage> <BreadcrumbPage>{t('Uptime.Breadcrumb.Uptime')}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</div> </div>
</header> </header>
<Toaster />
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-3xl font-bold">Uptime</span> <span className="text-3xl font-bold">{t('Uptime.Title')}</span>
<div className="flex gap-2">
<Select
value={String(itemsPerPage)}
onValueChange={handleItemsPerPageChange}
onOpenChange={(open) => {
if (open && customInputRef.current) {
customInputRef.current.value = String(itemsPerPage);
}
}}
>
<SelectTrigger className="w-[140px]">
<SelectValue>
{itemsPerPage} {itemsPerPage === 1 ? 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 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')} (custom)
</SelectItem>
) : null}
<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">{t('Common.ItemsPerPage.Custom')}</Label>
<div className="flex items-center gap-2 mt-1">
<Input
id="custom-items"
ref={customInputRef}
type="number"
min="1"
max="100"
className="h-8"
defaultValue={itemsPerPage}
onChange={(e) => {
const value = parseInt(e.target.value);
if (isNaN(value) || value < 1 || value > 100) {
e.target.classList.add("border-red-500");
} else {
e.target.classList.remove("border-red-500");
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (value >= 1 && value <= 100) {
handleItemsPerPageChange(e.target.value);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
const value = parseInt((e.target as HTMLInputElement).value);
if (value >= 1 && value <= 100) {
const validatedValue = Math.min(Math.max(value, 1), 100);
setItemsPerPage(validatedValue);
setPagination(prev => ({...prev, currentPage: 1}));
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
expires: 365,
path: "/",
sameSite: "strict",
});
getData(timespan, 1, validatedValue);
document.body.click();
}
}
}}
onClick={(e) => e.stopPropagation()}
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">{t('Common.ItemsPerPage.items')}</span>
</div>
</div>
</SelectContent>
</Select>
<Select <Select
value={String(timespan)} value={String(timespan)}
onValueChange={(v) => { onValueChange={(v) => {
@@ -164,20 +288,21 @@ export default function Uptime() {
disabled={isLoading} disabled={isLoading}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select timespan" /> <SelectValue placeholder={t('Uptime.TimeRange.Select')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="1">Last 1 hour</SelectItem> <SelectItem value="1">{t('Uptime.TimeRange.LastHour')}</SelectItem>
<SelectItem value="2">Last 1 day</SelectItem> <SelectItem value="2">{t('Uptime.TimeRange.LastDay')}</SelectItem>
<SelectItem value="3">Last 7 days</SelectItem> <SelectItem value="3">{t('Uptime.TimeRange.Last7Days')}</SelectItem>
<SelectItem value="4">Last 30 days</SelectItem> <SelectItem value="4">{t('Uptime.TimeRange.Last30Days')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
<div className="pt-4 space-y-4"> <div className="pt-4 space-y-4">
{isLoading ? ( {isLoading ? (
<div className="text-center py-8">Loading...</div> <div className="text-center py-8">{t('Uptime.Messages.Loading')}</div>
) : ( ) : (
data.map((app) => { data.map((app) => {
const reversedSummary = [...app.uptimeSummary].reverse(); const reversedSummary = [...app.uptimeSummary].reverse();
@@ -236,10 +361,10 @@ export default function Uptime() {
</p> </p>
<p> <p>
{entry.missing {entry.missing
? "No data" ? t('Uptime.Status.NoData')
: entry.online : entry.online
? "Online" ? t('Uptime.Status.Online')
: "Offline"} : t('Uptime.Status.Offline')}
</p> </p>
</div> </div>
<Tooltip.Arrow className="fill-gray-900" /> <Tooltip.Arrow className="fill-gray-900" />
@@ -260,6 +385,17 @@ export default function Uptime() {
{pagination.totalItems > 0 && !isLoading && ( {pagination.totalItems > 0 && !isLoading && (
<div className="pt-4 pb-4"> <div className="pt-4 pb-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-muted-foreground">
{pagination.totalItems > 0
? 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> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>

View File

@@ -2,6 +2,9 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider" 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({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -18,13 +21,17 @@ export const metadata: Metadata = {
description: "The only Dashboard you will need for your Services", description: "The only Dashboard you will need for your Services",
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const cookieStore = await cookies();
const locale = cookieStore.get('language')?.value || 'en';
const messages = (await import(`@/i18n/languages/${locale}.json`)).default;
return ( return (
<html lang="en"> <html lang={locale}>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
@@ -34,7 +41,9 @@ export default function RootLayout({
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<NextIntlClientProvider locale={locale} messages={messages}>
{children} {children}
</NextIntlClientProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@@ -14,8 +14,10 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {useTranslations} from 'next-intl';
export default function Home() { export default function Home() {
const t = useTranslations('Home');
const [username, setUsername] = useState("") const [username, setUsername] = useState("")
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const [rememberMe, setRememberMe] = useState(false) const [rememberMe, setRememberMe] = useState(false)
@@ -75,20 +77,20 @@ export default function Home() {
</div> </div>
</div> </div>
<h1 className="text-4xl font-bold tracking-tight text-foreground">CoreControl</h1> <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> </div>
<Card className="border-muted/40 shadow-lg"> <Card className="border-muted/40 shadow-lg">
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-2xl font-semibold">Login</CardTitle> <CardTitle className="text-2xl font-semibold">{t('LoginCardTitle')}</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription> <CardDescription>{t('LoginCardDescription')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{errorVisible && ( {errorVisible && (
<Alert variant="destructive" className="animate-in fade-in-50 slide-in-from-top-5"> <Alert variant="destructive" className="animate-in fade-in-50 slide-in-from-top-5">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Authentication Error</AlertTitle> <AlertTitle>{t('AuthenticationError')}</AlertTitle>
<AlertDescription>{error}</AlertDescription> <AlertDescription>{error}</AlertDescription>
</Alert> </Alert>
)} )}
@@ -96,7 +98,7 @@ export default function Home() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium"> <Label htmlFor="email" className="text-sm font-medium">
Email {t('Email')}
</Label> </Label>
<div className="relative"> <div className="relative">
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" /> <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="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="password" className="text-sm font-medium"> <Label htmlFor="password" className="text-sm font-medium">
Password {t('Password')}
</Label> </Label>
</div> </div>
<div className="relative"> <div className="relative">
@@ -138,7 +140,7 @@ export default function Home() {
<CardFooter className="flex flex-col space-y-4"> <CardFooter className="flex flex-col space-y-4">
<Button className="w-full" onClick={login} disabled={isLoading}> <Button className="w-full" onClick={login} disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign in"} {isLoading ? t('SigninButtonSigningIn') : t('SigninButton')}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>

View File

@@ -37,7 +37,7 @@ import { useRouter } from "next/navigation"
import packageJson from "@/package.json" import packageJson from "@/package.json"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { useTranslations } from "next-intl"
interface NavItem { interface NavItem {
title: string title: string
icon?: React.ComponentType<any> icon?: React.ComponentType<any>
@@ -46,49 +46,52 @@ interface NavItem {
items?: NavItem[] items?: NavItem[]
} }
const data: { navMain: NavItem[] } = {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const t = useTranslations('Sidebar')
const data: { navMain: NavItem[] } = {
navMain: [ navMain: [
{ {
title: "Dashboard", title: t('Dashboard'),
icon: LayoutDashboardIcon, icon: LayoutDashboardIcon,
url: "/dashboard", url: "/dashboard",
}, },
{ {
title: "My Infrastructure", title: t('My Infrastructure'),
url: "#", url: "#",
icon: Briefcase, icon: Briefcase,
items: [ items: [
{ {
title: "Servers", title: t('Servers'),
icon: Server, icon: Server,
url: "/dashboard/servers", url: "/dashboard/servers",
}, },
{ {
title: "Applications", title: t('Applications'),
icon: AppWindow, icon: AppWindow,
url: "/dashboard/applications", url: "/dashboard/applications",
}, },
{ {
title: "Uptime", title: t('Uptime'),
icon: Activity, icon: Activity,
url: "/dashboard/uptime", url: "/dashboard/uptime",
}, },
{ {
title: "Network", title: t('Network'),
icon: Network, icon: Network,
url: "/dashboard/network", url: "/dashboard/network",
}, },
], ],
}, },
{ {
title: "Settings", title: t('Settings'),
icon: Settings, icon: Settings,
url: "/dashboard/settings", url: "/dashboard/settings",
}, },
], ],
} }
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
@@ -115,7 +118,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton size="lg" asChild className="gap-3"> <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"> <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" /> <Image src="/logo.png" width={48} height={48} alt="CoreControl Logo" className="object-cover" />
</div> </div>
@@ -132,7 +135,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarContent className="flex flex-col h-full py-4"> <SidebarContent className="flex flex-col h-full py-4">
<SidebarGroup className="flex-grow"> <SidebarGroup className="flex-grow">
<SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2"> <SidebarGroupLabel className="text-xs font-medium text-sidebar-foreground/60 uppercase tracking-wider px-4 mb-2">
Main Navigation {t('Main Navigation')}
</SidebarGroupLabel> </SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
@@ -197,7 +200,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
onClick={logout} onClick={logout}
> >
<LogOut className="h-4 w-4 mr-2" /> <LogOut className="h-4 w-4 mr-2" />
Logout {t('Logout')}
</Button> </Button>
</SidebarFooter> </SidebarFooter>
</SidebarContent> </SidebarContent>

View File

@@ -54,6 +54,7 @@ export default defineConfig({
{ text: 'Gotify', link: '/notifications/Gotify' }, { text: 'Gotify', link: '/notifications/Gotify' },
{ text: 'Ntfy', link: '/notifications/Ntfy' }, { text: 'Ntfy', link: '/notifications/Ntfy' },
{ text: 'Pushover', link: '/notifications/Pushover' }, { text: 'Pushover', link: '/notifications/Pushover' },
{ text: 'Echobell', link: '/notifications/Echobell' },
] ]
} }
], ],

View File

@@ -8,8 +8,8 @@
<meta name="generator" content="VitePress v1.6.3"> <meta name="generator" content="VitePress v1.6.3">
<link rel="preload stylesheet" href="/assets/style.DEOyzpKL.css" as="style"> <link rel="preload stylesheet" href="/assets/style.DEOyzpKL.css" as="style">
<link rel="preload stylesheet" href="/vp-icons.css" as="style"> <link rel="preload stylesheet" href="/vp-icons.css" as="style">
<script type="module" src="/assets/chunks/metadata.b7f24d28.js"></script> <script type="module" src="/assets/chunks/metadata.80a3dac1.js"></script>
<script type="module" src="/assets/app.DQZaLSC2.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="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="icon" type="image/png" href="/logo.png"> <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-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>

View File

@@ -1 +1 @@
import{t as p}from"./chunks/theme.BTnOYcHU.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.DPDPlp3K.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp}; import{t as p}from"./chunks/theme.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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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}");

View File

@@ -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\":\"_yXl4OkC\",\"installation.md\":\"Cz1eOHOr\",\"notifications_discord.md\":\"C0x5CxmR\",\"notifications_email.md\":\"Cugw2BRs\",\"notifications_general.md\":\"D7AVsSjD\",\"notifications_gotify.md\":\"vFHjr6ko\",\"notifications_ntfy.md\":\"CPMnGQVP\",\"notifications_pushover.md\":\"lZwGAQ0A\",\"notifications_telegram.md\":\"B6_EzaEX\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"CoreControl\",\"description\":\"Dashboard to manage your entire server infrastructure\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"logo\":\"/logo.png\",\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Installation\",\"link\":\"/installation\"}],\"footer\":{\"message\":\"Released under the MIT License.\",\"copyright\":\"Copyright © 2025-present CoreControl\"},\"search\":{\"provider\":\"local\"},\"sidebar\":[{\"text\":\"Deploy\",\"items\":[{\"text\":\"Installation\",\"link\":\"/installation\"}]},{\"text\":\"General\",\"items\":[{\"text\":\"Dashboard\",\"link\":\"/general/Dashboard\"},{\"text\":\"Servers\",\"link\":\"/general/Servers\"},{\"text\":\"Applications\",\"link\":\"/general/Applications\"},{\"text\":\"Uptime\",\"link\":\"/general/Uptime\"},{\"text\":\"Network\",\"link\":\"/general/Network\"},{\"text\":\"Settings\",\"link\":\"/general/Settings\"}]},{\"text\":\"Notifications\",\"items\":[{\"text\":\"General\",\"link\":\"/notifications/General\"},{\"text\":\"Email\",\"link\":\"/notifications/Email\"},{\"text\":\"Telegram\",\"link\":\"/notifications/Telegram\"},{\"text\":\"Discord\",\"link\":\"/notifications/Discord\"},{\"text\":\"Gotify\",\"link\":\"/notifications/Gotify\"},{\"text\":\"Ntfy\",\"link\":\"/notifications/Ntfy\"},{\"text\":\"Pushover\",\"link\":\"/notifications/Pushover\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/crocofied/corecontrol\"},{\"icon\":\"buymeacoffee\",\"link\":\"https://www.buymeacoffee.com/corecontrol\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":true}");

View 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 &quot;Settings&quot;"></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 &quot;User Settings&quot;"></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 &quot;Theme Settings&quot;"></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 &quot;Language Settings&quot;"></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 &quot;Notification Settings&quot;"></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};

View 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};

View File

@@ -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 &quot;Settings&quot;"></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 &quot;User Settings&quot;"></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 &quot;Theme Settings&quot;"></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 &quot;Notification Settings&quot;"></a></h2><p><img src="'+e+'" alt="Notification Settings"></p>',8)]))}const u=s(g,[["render",l]]);export{f as __pageData,u as default};

View File

@@ -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};

View File

@@ -1 +1 @@
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed withalerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default}; import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed with alerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":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};

View File

@@ -1 +1 @@
import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed withalerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1745241416000}'),n={name:"index.md"};function i(o,r,s,c,l,d){return a(),t("div")}const p=e(n,[["render",i]]);export{u as __pageData,p as default}; import{_ as e,c as t,o as a}from"./chunks/framework.DPDPlp3K.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"CoreControl","text":"Manage your server infrastructure","actions":[{"theme":"brand","text":"Install","link":"/installation"},{"theme":"alt","text":"GitHub","link":"https://github.com/crocofied/corecontrol"}],"image":{"src":"/logo.png","alt":"Logo"}},"features":[{"icon":"🚀","title":"Easy Deployment","details":"Deploy and manage your servers with just a few clicks - thanks to docker"},{"icon":"🔒","title":"Secure Management","details":"Secure connections with the panel and a more secure authentication system"},{"icon":"📊","title":"Real-time Monitoring","details":"Monitor server performance, resource usage and uptime in real-time"},{"icon":"🎮","title":"Easy to Manage","details":"Simple and intuitive management interface for all your needs"},{"icon":"🔔","title":"Notifications","details":"Stay informed with alerts and notifications about your servers & applications status"},{"icon":"✨","title":"Clean UI","details":"Modern and user-friendly interface designed for the best user experience"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":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};

View File

@@ -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 &quot;Installation&quot;"></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 &quot;Docker Compose Installation&quot;"></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 &quot;Installation&quot;"></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 &quot;Docker Compose Installation&quot;"></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;"> 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;"> image</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">haedlessdev/corecontrol:latest</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> ports</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:</span></span> <span class="line"><span style="--shiki-light:#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;">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:#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:#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 &quot;Authentication&quot;"></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 &quot;Authentication&quot;"></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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View 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};

View 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};

View 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 &quot;Echobell Notification Setup&quot;"></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. Youll find it in your channels <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 &quot;How to get your Webhook URL and field key&quot;"></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};

View 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};

View File

@@ -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};

View File

@@ -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};

View 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 &quot;Email Notification Setup&quot;"></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&#39;s email address (e.g., <code>noreply@example.com</code>).<br><em>→ This address will appear as the sender in the recipient&#39;s inbox.</em></p></li><li><p><strong>To Address</strong><br> The recipient&#39;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};

View 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};

View 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 &quot;Gotify Notification Setup&quot;"></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 &quot;How to get these values:&quot;"></a></h2><ol><li><strong>Log in to your Gotify server.</strong></li><li><strong>Go to the &quot;Applications&quot; section.</strong></li><li><strong>Create a new application</strong> (e.g., &quot;System Alerts&quot;).</li><li><strong>Copy the generated token</strong> — this is your Gotify Token.</li><li><strong>Use your server&#39;s URL</strong> as the Gotify URL.</li></ol>',5)]))}const h=o(n,[["render",s]]);export{g as __pageData,h as default};

View 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};

View File

@@ -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};

View File

@@ -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};

View 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 &quot;ntfy Notification Setup&quot;"></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 &quot;How to get the ntfy URL and Token&quot;"></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://&lt;your-ntfy-server&gt;/&lt;your-topic&gt;</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;">&quot;notifications&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</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;">&gt;</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;"> &lt;</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;">&gt;</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&#39;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 &quot;Example Token Management Commands&quot;"></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 &#39;alice&#39;</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 &#39;alice&#39; (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 &#39;bob&#39;, 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};

View 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};

View File

@@ -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};

View File

@@ -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};

View 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 &quot;Pushover Notification Setup&quot;"></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 applications 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 &quot;How to get the URL, Token, and User Key&quot;"></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 accounts 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};

View 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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View 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 &quot;Telegram Notification Setup&quot;"></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 &quot;How to create the bot and get the Bot Token&quot;"></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 &quot;Example: getUpdates response excerpt&quot;"></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;"> &quot;ok&quot;</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;"> &quot;result&quot;</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;"> &quot;update_id&quot;</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;"> &quot;message&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> &quot;message_id&quot;</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;"> &quot;from&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">&quot;id&quot;</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;">&quot;is_bot&quot;</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;">&quot;first_name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;User&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> &quot;chat&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">&quot;id&quot;</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;">&quot;first_name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;User&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">&quot;type&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;private&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> &quot;date&quot;</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;"> &quot;text&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Hello&quot;</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};

View 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};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"general_applications.md":"DFVqSlCw","general_dashboard.md":"DW5yESFW","general_network.md":"tbP8aEzX","general_servers.md":"BaASA60T","general_settings.md":"DrC2XV32","general_uptime.md":"CKBdQg4u","index.md":"_yXl4OkC","installation.md":"Cz1eOHOr","notifications_discord.md":"C0x5CxmR","notifications_email.md":"Cugw2BRs","notifications_general.md":"D7AVsSjD","notifications_gotify.md":"vFHjr6ko","notifications_ntfy.md":"CPMnGQVP","notifications_pushover.md":"lZwGAQ0A","notifications_telegram.md":"B6_EzaEX"} {"general_applications.md":"DFVqSlCw","general_dashboard.md":"DW5yESFW","general_network.md":"tbP8aEzX","general_servers.md":"BaASA60T","general_settings.md":"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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More