diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 5d01849..0000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -npm-debug.log -.env -agent/ -.next -docs/ -screenshots/ \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 15742aa..0000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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/). diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 0df7837..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,8 +0,0 @@ -# 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. \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index f20fe03..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: crocofied -buy_me_a_coffee: corecontrol diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index c27febb..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,67 +0,0 @@ -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... diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index e48ff4f..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,41 +0,0 @@ -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... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index fff3906..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,7 +0,0 @@ -## 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 diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index ec4bb38..0000000 --- a/.github/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cedeca7..0000000 --- a/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - - -# Vitepress -docs/.vitepress/cache -docs/node_modules/ - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3bea224..0000000 --- a/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -# Builder Stage -FROM --platform=$BUILDPLATFORM node:20-alpine AS builder - -ARG TARGETARCH # Automatically set by Buildx - -WORKDIR /app - -COPY package.json package-lock.json* ./ -COPY ./prisma ./prisma - -# Set PRISMA_CLI_BINARY_TARGETS based on TARGETARCH and install dependencies -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 - -COPY . . -RUN npm run build - -# Production Stage -FROM --platform=$TARGETPLATFORM node:20-alpine AS production - -WORKDIR /app - -ENV NODE_ENV production - -# Copy built assets and dependencies from builder -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/public ./public -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/next.config.js* ./ - -# Prune dev dependencies -RUN npm prune --production - -EXPOSE 3000 - -# Dynamically set PRISMA_CLI_BINARY_TARGETS based on runtime architecture and 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"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index aaff6f8..0000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2025 Damian Bergemann - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index eb9f87a..0000000 --- a/README.md +++ /dev/null @@ -1,115 +0,0 @@ - -![Logo](https://i.ibb.co/hwSZTJH/Kopie-von-Cash-Mate.png) - - -# CoreControl - -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) -[![Sponsor](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)](https://github.com/sponsors/crocofied) - -## Features - -- Dashboard: A clear screen with all the important information about your servers (WIP) -- Servers: This allows you to add all your servers (including Hardware Information), with Quicklinks to their Management Panels -- Applications: Add all your self-hosted services to a clear list and track their up and down time -- Networks: Generate visually stunning network flowcharts with ease. - -## Screenshots -Login Page: -![Login Page](/screenshots/login.png) - -Dashboard Page: -![Dashboard Page](/screenshots/dashboard.png) - -Servers Page: -![Servers Page](/screenshots/servers.png) - -Server Detail Page -![Server Detail Page](/screenshots/server.png) - -Applications Page: -![Applications Page](/screenshots/applications.png) - -Uptime Page: -![Uptime Page](/screenshots/uptime.png) - -Network Page: -![Network Page](/screenshots/network.png) - -Settings Page: -![Settings Page](/screenshots/settings.png) - -## Roadmap -- [X] Edit Applications, Applications searchbar -- [X] Uptime History -- [X] Notifications -- [X] Simple Server Monitoring -- [ ] Improved Network Flowchart with custom elements (like Network switches) -- [ ] Advanced Settings (Disable Uptime Tracking & more) - -## Deployment - -Simply run this compose.yml: -```yml -services: - web: - image: haedlessdev/corecontrol:latest - ports: - - "3000:3000" - environment: - JWT_SECRET: RANDOM_SECRET # Replace with a secure random string - DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" - - agent: - image: haedlessdev/corecontrol-agent:latest - environment: - DATABASE_URL: "postgresql://postgres:postgres@db:5432/postgres" - depends_on: - db: - condition: service_healthy - - db: - image: postgres:17 - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 2s - timeout: 2s - retries: 10 - -volumes: - postgres_data: -``` - -#### Default Login -__E-Mail:__ admin@example.com\ -__Password:__ admin - -## Tech Stack & Credits - -The application is build with: -- Next.js & Typescript -- Go (used for the agent) -- Tailwindcss with [shadcn](shadcn.com) -- PostgreSQL with [Prisma ORM](https://www.prisma.io/) -- Icons by [Lucide](https://lucide.dev/) -- Flowcharts by [React Flow](https://reactflow.dev/) -- Application icons by [selfh.st/icons](https://selfh.st/icons) -- Monitoring Tool by [Glances](https://github.com/nicolargo/glances) -- and a lot of love ❤️ - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=crocofied/CoreControl&type=Date)](https://www.star-history.com/#crocofied/CoreControl&Date) - -## License - -Licensed under the [MIT License](https://github.com/crocofied/CoreControl/blob/main/LICENSE). diff --git a/agent/.dockerignore b/agent/.dockerignore deleted file mode 100644 index 2eea525..0000000 --- a/agent/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.env \ No newline at end of file diff --git a/agent/Dockerfile b/agent/Dockerfile deleted file mode 100644 index 2fbaea4..0000000 --- a/agent/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# --- Build Stage --- -# Multi-Arch Builder mit expliziter Plattform-Angabe -FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS builder - -ARG TARGETOS TARGETARCH - -WORKDIR /app - -ENV GO111MODULE=on \ - CGO_ENABLED=0 \ - GOOS=$TARGETOS \ - GOARCH=$TARGETARCH - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -# Cross-Compile für Zielarchitektur -RUN go build -ldflags="-w -s" -o app ./cmd/agent - -# --- Run Stage --- -# Multi-Arch Laufzeit-Image -FROM alpine:latest - -# Notwendig für TLS/SSL-Zertifikate -RUN apk --no-cache add ca-certificates gcompat - -WORKDIR /root/ - -COPY --from=builder /app/app . - -# Security Hardening -USER nobody:nobody -ENV GOMAXPROCS=1 - -CMD ["./app"] - -# - - BUILD COMMAND - - -# docker buildx build \ -# --platform linux/amd64,linux/arm64,linux/arm/v7 \ -# -t haedlessdev/corecontrol-agent:1.0.0 \ -# -t haedlessdev/corecontrol-agent:latest \ -# --push \ -# . \ No newline at end of file diff --git a/agent/cmd/agent/main.go b/agent/cmd/agent/main.go deleted file mode 100644 index 5343f13..0000000 --- a/agent/cmd/agent/main.go +++ /dev/null @@ -1,111 +0,0 @@ -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) - } -} diff --git a/agent/go.mod b/agent/go.mod deleted file mode 100644 index a991b61..0000000 --- a/agent/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module github.com/corecontrol/agent - -go 1.19 - -require ( - github.com/jackc/pgx/v4 v4.18.1 - github.com/joho/godotenv v1.5.1 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df -) - -require ( - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.0 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.2 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.14.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/text v0.7.0 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect -) diff --git a/agent/go.sum b/agent/go.sum deleted file mode 100644 index dc0a3fd..0000000 --- a/agent/go.sum +++ /dev/null @@ -1,205 +0,0 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q= -github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/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.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= -github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= -github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/agent/internal/app/monitor.go b/agent/internal/app/monitor.go deleted file mode 100644 index 4303971..0000000 --- a/agent/internal/app/monitor.go +++ /dev/null @@ -1,130 +0,0 @@ -package app - -import ( - "context" - "crypto/x509" - "database/sql" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "strings" - "time" - - "github.com/corecontrol/agent/internal/models" - "github.com/corecontrol/agent/internal/notifications" -) - -// MonitorApplications checks and updates the status of all applications -func MonitorApplications(db *sql.DB, client *http.Client, apps []models.Application, notifSender *notifications.NotificationSender) { - var notificationTemplate string - err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate) - if err != nil || notificationTemplate == "" { - notificationTemplate = "The application !name (!url) went !status!" - } - - for _, app := range apps { - logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL) - fmt.Printf("%s Checking...\n", logPrefix) - - // Determine which URL to use for monitoring - checkURL := app.PublicURL - if app.UptimeCheckURL != "" { - checkURL = app.UptimeCheckURL - fmt.Printf("%s Using custom uptime check URL: %s\n", logPrefix, checkURL) - } - - parsedURL, parseErr := url.Parse(checkURL) - if parseErr != nil { - fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr) - continue - } - - hostIsIP := isIPAddress(parsedURL.Hostname()) - var isOnline bool - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, "GET", checkURL, nil) - if err != nil { - fmt.Printf("%s Request creation failed: %v\n", logPrefix, err) - continue - } - - resp, err := client.Do(req) - if err == nil { - defer resp.Body.Close() - isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400 - fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode) - } else { - fmt.Printf("%s Connection error: %v\n", logPrefix, err) - - if hostIsIP { - var urlErr *url.Error - if errors.As(err, &urlErr) { - var certErr x509.HostnameError - var unknownAuthErr x509.UnknownAuthorityError - if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) { - fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix) - isOnline = true - } - } - } - } - - if isOnline != app.Online { - status := "offline" - if isOnline { - status = "online" - } - - message := strings.ReplaceAll(notificationTemplate, "!name", app.Name) - message = strings.ReplaceAll(message, "!url", app.PublicURL) - message = strings.ReplaceAll(message, "!status", status) - - notifSender.SendNotifications(message) - } - - // Update application status in database - updateApplicationStatus(db, app.ID, isOnline) - - // Add entry to uptime history - addUptimeHistoryEntry(db, app.ID, isOnline) - } -} - -// Helper function to update application status -func updateApplicationStatus(db *sql.DB, appID int, online bool) { - dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer dbCancel() - - _, err := db.ExecContext(dbCtx, - `UPDATE application SET online = $1 WHERE id = $2`, - online, appID, - ) - if err != nil { - fmt.Printf("DB update failed for app ID %d: %v\n", appID, err) - } -} - -// Helper function to add uptime history entry -func addUptimeHistoryEntry(db *sql.DB, appID int, online bool) { - dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer dbCancel() - - _, err := db.ExecContext(dbCtx, - `INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`, - appID, online, - ) - if err != nil { - fmt.Printf("History insert failed for app ID %d: %v\n", appID, err) - } -} - -// Helper function to check if a host is an IP address -func isIPAddress(host string) bool { - ip := net.ParseIP(host) - return ip != nil -} diff --git a/agent/internal/database/database.go b/agent/internal/database/database.go deleted file mode 100644 index 5423707..0000000 --- a/agent/internal/database/database.go +++ /dev/null @@ -1,198 +0,0 @@ -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, ¬ificationId); err != nil { - fmt.Printf("Error scanning test notification: %v\n", err) - continue - } - - // Add to list of IDs to delete - testIds = append(testIds, id) - - // Find the notification configuration - for _, n := range notifications { - if n.ID == notificationId { - // Send test notification - fmt.Printf("Sending test notification to notification ID %d\n", notificationId) - sendFunc(n, "Test notification from CoreControl") - } - } - } - - // Delete processed test notifications - if len(testIds) > 0 { - for _, id := range testIds { - _, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id) - if err != nil { - fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err) - } - } - } -} diff --git a/agent/internal/models/models.go b/agent/internal/models/models.go deleted file mode 100644 index 6bb27e1..0000000 --- a/agent/internal/models/models.go +++ /dev/null @@ -1,98 +0,0 @@ -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 -} diff --git a/agent/internal/notifications/notifications.go b/agent/internal/notifications/notifications.go deleted file mode 100644 index 0e9ca28..0000000 --- a/agent/internal/notifications/notifications.go +++ /dev/null @@ -1,266 +0,0 @@ -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) - } -} diff --git a/agent/internal/server/monitor.go b/agent/internal/server/monitor.go deleted file mode 100644 index 2403594..0000000 --- a/agent/internal/server/monitor.go +++ /dev/null @@ -1,381 +0,0 @@ -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(¬ificationTemplate) - if err != nil || notificationTemplate == "" { - notificationTemplate = "The server !name is now !status!" - } - - for _, server := range servers { - if !server.Monitoring || !server.MonitoringURL.Valid { - continue - } - - logPrefix := fmt.Sprintf("[Server %s]", server.Name) - fmt.Printf("%s Checking...\n", logPrefix) - - baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/") - var cpuUsage, ramUsage, diskUsage, 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") -} diff --git a/app/api/applications/add/route.ts b/app/api/applications/add/route.ts deleted file mode 100644 index b5e5f12..0000000 --- a/app/api/applications/add/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface AddRequest { - serverId: number; - name: string; - description: string; - icon: string; - publicURL: string; - localURL: string; - uptimecheckUrl: string; -} - -export async function POST(request: NextRequest) { - try { - const body: AddRequest = await request.json(); - const { serverId, name, description, icon, publicURL, localURL, uptimecheckUrl } = body; - - const application = await prisma.application.create({ - data: { - serverId, - name, - description, - icon, - publicURL, - localURL, - uptimecheckUrl - } - }); - - return NextResponse.json({ message: "Success", application }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/applications/delete/route.ts b/app/api/applications/delete/route.ts deleted file mode 100644 index a3132f3..0000000 --- a/app/api/applications/delete/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const id = Number(body.id); - - if (!id) { - return NextResponse.json({ error: "Missing ID" }, { status: 400 }); - } - - await prisma.application.delete({ - where: { id: id } - }); - - await prisma.uptime_history.deleteMany({ - where: { applicationId: id } - }); - - return NextResponse.json({ success: true }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/applications/edit/route.ts b/app/api/applications/edit/route.ts deleted file mode 100644 index 48bd91c..0000000 --- a/app/api/applications/edit/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface EditRequest { - id: number; - name: string; - description: string; - serverId: number; - icon: string; - publicURL: string; - localURL: string; - uptimecheckUrl: string; -} - -export async function PUT(request: NextRequest) { - try { - const body: EditRequest = await request.json(); - const { id, name, description, serverId, icon, publicURL, localURL, uptimecheckUrl } = body; - - const existingApp = await prisma.application.findUnique({ where: { id } }); - if (!existingApp) { - return NextResponse.json({ error: "Server not found" }, { status: 404 }); - } - - const updatedApplication = await prisma.application.update({ - where: { id }, - data: { - serverId, - name, - description, - icon, - publicURL, - localURL, - uptimecheckUrl - } - }); - - return NextResponse.json({ message: "Application updated", application: updatedApplication }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/applications/get/route.ts b/app/api/applications/get/route.ts deleted file mode 100644 index ac4bf44..0000000 --- a/app/api/applications/get/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface PostRequest { - page?: number; - ITEMS_PER_PAGE?: number; -} - -export async function POST(request: NextRequest) { - try { - const body: PostRequest = await request.json(); - const page = Math.max(1, body.page || 1); - const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 10; - - const [applications, totalCount, servers_all] = await Promise.all([ - prisma.application.findMany({ - skip: (page - 1) * ITEMS_PER_PAGE, - take: ITEMS_PER_PAGE, - orderBy: { name: "asc" } - }), - prisma.application.count(), - prisma.server.findMany() - ]); - - const serverIds = applications - .map((app: { serverId: number | null }) => app.serverId) - .filter((id:any): id is number => id !== null); - - const servers = await prisma.server.findMany({ - where: { id: { in: serverIds } } - }); - - const applicationsWithServers = applications.map((app: any) => ({ - ...app, - server: servers.find((s: any) => s.id === app.serverId)?.name || "No server" - })); - - const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE); - - return NextResponse.json({ - applications: applicationsWithServers, - servers: servers_all, - maxPage, - totalItems: totalCount - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : "Unknown error"; - return NextResponse.json({ error: message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/applications/search/route.ts b/app/api/applications/search/route.ts deleted file mode 100644 index c2d7ef2..0000000 --- a/app/api/applications/search/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; -import Fuse from "fuse.js"; - -interface SearchRequest { - searchterm: string; -} - -export async function POST(request: NextRequest) { - try { - const body: SearchRequest = await request.json(); - const { searchterm } = body; - - const applications = await prisma.application.findMany({}); - - const fuseOptions = { - keys: ['name', 'description'], - threshold: 0.3, - includeScore: true, - }; - - const fuse = new Fuse(applications, fuseOptions); - - const searchResults = fuse.search(searchterm); - - const searchedApps = searchResults.map(({ item }) => item); - - // Get server IDs from the search results - const serverIds = searchedApps - .map(app => app.serverId) - .filter((id): id is number => id !== null); - - // Fetch server data for these applications - const servers = await prisma.server.findMany({ - where: { id: { in: serverIds } } - }); - - // Add server name to each application - const results = searchedApps.map(app => ({ - ...app, - server: servers.find(s => s.id === app.serverId)?.name || "No server" - })); - - return NextResponse.json({ results }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/applications/uptime/route.ts b/app/api/applications/uptime/route.ts deleted file mode 100644 index a42f545..0000000 --- a/app/api/applications/uptime/route.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface RequestBody { - timespan?: number; - page?: number; - itemsPerPage?: number; - } - - -const getTimeRange = (timespan: number) => { - const now = new Date(); - switch (timespan) { - case 1: - return { - start: new Date(now.getTime() - 60 * 60 * 1000), - interval: 'minute' - }; - case 2: - return { - start: new Date(now.getTime() - 24 * 60 * 60 * 1000), - interval: 'hour' - }; - case 3: - return { - start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), - interval: 'day' - }; - case 4: - return { - start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), - interval: 'day' - }; - default: - return { - start: new Date(now.getTime() - 60 * 60 * 1000), - interval: 'minute' - }; - } -}; - -const generateIntervals = (timespan: number) => { - const now = new Date(); - now.setSeconds(0, 0); - - switch (timespan) { - case 1: // 1 hour - 60 one-minute intervals - return Array.from({ length: 60 }, (_, i) => { - const d = new Date(now); - d.setMinutes(d.getMinutes() - i); - d.setSeconds(0, 0); - return d; - }); - - case 2: // 1 day - 24 one-hour intervals - return Array.from({ length: 24 }, (_, i) => { - const d = new Date(now); - d.setHours(d.getHours() - i); - d.setMinutes(0, 0, 0); - return d; - }); - - case 3: // 7 days - return Array.from({ length: 7 }, (_, i) => { - const d = new Date(now); - d.setDate(d.getDate() - i); - d.setHours(0, 0, 0, 0); - return d; - }); - - case 4: // 30 days - return Array.from({ length: 30 }, (_, i) => { - const d = new Date(now); - d.setDate(d.getDate() - i); - d.setHours(0, 0, 0, 0); - return d; - }); - - default: - return []; - } -}; - -const getIntervalKey = (date: Date, timespan: number) => { - const d = new Date(date); - switch (timespan) { - case 1: // 1 hour - minute intervals - d.setSeconds(0, 0); - return d.toISOString(); - case 2: // 1 day - hour intervals - d.setMinutes(0, 0, 0); - return d.toISOString(); - case 3: // 7 days - day intervals - case 4: // 30 days - day intervals - d.setHours(0, 0, 0, 0); - return d.toISOString(); - default: - return d.toISOString(); - } -}; - -export async function POST(request: NextRequest) { - try { - const { timespan = 1, page = 1, itemsPerPage = 5 }: RequestBody = await request.json(); - const skip = (page - 1) * itemsPerPage; - - // Get paginated and sorted applications - const [applications, totalCount] = await Promise.all([ - prisma.application.findMany({ - skip, - take: itemsPerPage, - orderBy: { name: 'asc' } - }), - prisma.application.count() - ]); - - const applicationIds = applications.map(app => app.id); - - // Get time range and intervals - const { start } = getTimeRange(timespan); - const intervals = generateIntervals(timespan); - - // Get uptime history for the filtered applications - const uptimeHistory = await prisma.uptime_history.findMany({ - where: { - applicationId: { in: applicationIds }, - createdAt: { gte: start } - }, - orderBy: { createdAt: "desc" } - }); - - // Process data for each application - const result = applications.map(app => { - const appChecks = uptimeHistory.filter(check => check.applicationId === app.id); - const checksMap = new Map(); - - for (const check of appChecks) { - const intervalKey = getIntervalKey(check.createdAt, timespan); - const current = checksMap.get(intervalKey) || { failed: 0, total: 0 }; - current.total++; - if (!check.online) current.failed++; - checksMap.set(intervalKey, current); - } - - const uptimeSummary = intervals.map(interval => { - const intervalKey = getIntervalKey(interval, timespan); - const stats = checksMap.get(intervalKey); - - return { - timestamp: intervalKey, - missing: !stats, - online: stats ? (stats.failed / stats.total) <= 0.5 : null - }; - }); - - return { - appName: app.name, - appId: app.id, - uptimeSummary - }; - }); - - return NextResponse.json({ - data: result, - pagination: { - currentPage: page, - totalPages: Math.ceil(totalCount / itemsPerPage), - totalItems: totalCount - } - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : "Unknown error"; - return NextResponse.json({ error: message }, { status: 500 }); - } - } \ No newline at end of file diff --git a/app/api/auth/edit_email/route.ts b/app/api/auth/edit_email/route.ts deleted file mode 100644 index 73fccd1..0000000 --- a/app/api/auth/edit_email/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import jwt from 'jsonwebtoken'; -import { prisma } from "@/lib/prisma"; - -interface EditEmailRequest { - newEmail: string; - jwtToken: string; -} - -export async function POST(request: NextRequest) { - try { - const body: EditEmailRequest = await request.json(); - const { newEmail, jwtToken } = body; - - // Ensure JWT_SECRET is defined - if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET is not defined'); - } - - // Verify JWT - const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string }; - if (!decoded.account_secret) { - return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); - } - - // Get the user by account id - const user = await prisma.user.findUnique({ - where: { id: decoded.account_secret }, - }); - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }); - } - - - // Check if the new email is already in use - const existingUser = await prisma.user.findUnique({ - where: { email: newEmail }, - }); - - if (existingUser) { - return NextResponse.json({ error: 'Email already in use' }, { status: 400 }); - } - - // Update the user's email - await prisma.user.update({ - where: { id: user.id }, - data: { email: newEmail }, - }); - - - return NextResponse.json({ message: 'Email updated successfully' }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/auth/edit_password/route.ts b/app/api/auth/edit_password/route.ts deleted file mode 100644 index 677a9af..0000000 --- a/app/api/auth/edit_password/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import jwt from 'jsonwebtoken'; -import { prisma } from "@/lib/prisma"; -import bcrypt from 'bcryptjs'; - -interface EditEmailRequest { - oldPassword: string; - newPassword: string; - jwtToken: string; -} - -export async function POST(request: NextRequest) { - try { - const body: EditEmailRequest = await request.json(); - const { oldPassword, newPassword, jwtToken } = body; - - // Ensure JWT_SECRET is defined - if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET is not defined'); - } - - // Verify JWT - const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET) as { account_secret: string }; - if (!decoded.account_secret) { - return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); - } - - // Get the user by account id - const user = await prisma.user.findUnique({ - where: { id: decoded.account_secret }, - }); - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }); - } - - // Check if the old password is correct - const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password); - if (!isOldPasswordValid) { - return NextResponse.json({ error: 'Old password is incorrect' }, { status: 401 }); - } - - // Hash the new password - const hashedNewPassword = await bcrypt.hash(newPassword, 10); - // Update the user's password - await prisma.user.update({ - where: { id: user.id }, - data: { password: hashedNewPassword }, - }); - - return NextResponse.json({ message: 'Password updated successfully' }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts deleted file mode 100644 index 3498043..0000000 --- a/app/api/auth/login/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import jwt from 'jsonwebtoken'; -import { prisma } from "@/lib/prisma"; -import bcrypt from 'bcryptjs'; - -interface LoginRequest { - username: string; - password: string; -} - -export async function POST(request: NextRequest) { - try { - const body: LoginRequest = await request.json(); - const { username, password } = body; - - // Ensure JWT_SECRET is defined - if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET is not defined'); - } - - let accountId: string = ''; - // Check if there are any entries in user - const userCount = await prisma.user.count(); - if (userCount === 0) { - if(username=== "admin@example.com" && password === "admin") { - // Hash the password - const hashedPassword = await bcrypt.hash(password, 10); - // Create the first user with hashed password - const user = await prisma.user.create({ - data: { - email: username, - password: hashedPassword, - }, - }); - - // Get the account id - accountId = user.id; - } else { - return NextResponse.json({ error: "Wrong credentials" }, { status: 401 }); - } - } else { - // Get the user by username - const user = await prisma.user.findUnique({ - where: { email: username }, - }); - if (!user) { - return NextResponse.json({ error: "Wrong credentials" }, { status: 401 }); - } - // Check if the password is correct - const isPasswordValid = await bcrypt.compare(password, user.password); - if (!isPasswordValid) { - return NextResponse.json({ error: "Wrong credentials" }, { status: 401 }); - } - // Get the account id - accountId = user.id; - } - - // Create JWT - const token = jwt.sign({ account_secret: accountId }, process.env.JWT_SECRET, { expiresIn: '7d' }); - - return NextResponse.json({ token }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts deleted file mode 100644 index 0b25efa..0000000 --- a/app/api/auth/validate/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import jwt, { JwtPayload } from 'jsonwebtoken'; -import { prisma } from "@/lib/prisma"; - -interface ValidateRequest { - token: string; -} - -export async function POST(request: NextRequest) { - try { - const body: ValidateRequest = await request.json(); - const { token } = body; - - // Ensure JWT_SECRET is defined - if (!process.env.JWT_SECRET) { - throw new Error('JWT_SECRET is not defined'); - } - - // Get the account id - const user = await prisma.user.findFirst({ - where: {}, - }); - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }); - } - - // Verify JWT - const decoded = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload & { id: string }; - - if(!decoded.account_secret) { - return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); - } - - if(decoded.account_secret !== user.id) { - return NextResponse.json({ error: 'Invalid token' }, { status: 400 }); - } - - - return NextResponse.json({ message: 'Valid token' }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/dashboard/get/route.ts b/app/api/dashboard/get/route.ts deleted file mode 100644 index c8d6949..0000000 --- a/app/api/dashboard/get/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -export async function POST(request: NextRequest) { - try { - const serverCountNoVMs = await prisma.server.count({ - where: { - hostServer: 0 - } - }); - - const serverCountOnlyVMs = await prisma.server.count({ - where: { - hostServer: { - not: 0 - } - } - }); - - const applicationCount = await prisma.application.count(); - - const onlineApplicationsCount = await prisma.application.count({ - where: { online: true } - }); - - return NextResponse.json({ - serverCountNoVMs, - serverCountOnlyVMs, - applicationCount, - onlineApplicationsCount - }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/flowchart/route.ts b/app/api/flowchart/route.ts deleted file mode 100644 index 4874d16..0000000 --- a/app/api/flowchart/route.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface Node { - id: string; - type: string; - data: { - label: string; - [key: string]: any; - }; - position: { x: number; y: number }; - style: React.CSSProperties; - draggable?: boolean; - selectable?: boolean; - zIndex?: number; -} - -interface Edge { - id: string; - source: string; - target: string; - type: string; - style: { - stroke: string; - strokeWidth: number; - }; -} - -interface Server { - id: number; - name: string; - ip: string; - host: boolean; - hostServer: number | null; -} - -interface Application { - id: number; - name: string; - localURL: string; - serverId: number; -} - -const NODE_WIDTH = 220; -const NODE_HEIGHT = 60; -const APP_NODE_WIDTH = 160; -const APP_NODE_HEIGHT = 40; -const HORIZONTAL_SPACING = 700; -const VERTICAL_SPACING = 80; -const START_Y = 120; -const ROOT_NODE_WIDTH = 300; -const CONTAINER_PADDING = 40; -const COLUMN_SPACING = 220; -const VM_APP_SPACING = 220; -const MIN_VM_SPACING = 10; -const APP_ROW_SPACING = 15; - -export async function GET() { - try { - const [servers, applications] = await Promise.all([ - prisma.server.findMany({ - orderBy: { id: "asc" }, - }) as Promise, - prisma.application.findMany({ - orderBy: { serverId: "asc" }, - }) as Promise, - ]); - - // Level 2: Physical Servers - const serverNodes: Node[] = servers - .filter(server => !server.hostServer) - .map((server, index, filteredServers) => { - const xPos = - index * HORIZONTAL_SPACING - - ((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2; - - return { - id: `server-${server.id}`, - type: "server", - data: { - label: `${server.name}\n${server.ip}`, - ...server, - }, - position: { x: xPos, y: START_Y }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "4px", - padding: "8px", - width: NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "0.9rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", - }, - }; - }); - - // Level 3: Services and VMs - const serviceNodes: Node[] = []; - const vmNodes: Node[] = []; - - servers.forEach((server) => { - const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`); - if (serverNode) { - const serverX = serverNode.position.x; - - // Services (left column) - applications - .filter(app => app.serverId === server.id) - .forEach((app, appIndex) => { - serviceNodes.push({ - id: `service-${app.id}`, - type: "service", - data: { - label: `${app.name}\n${app.localURL}`, - ...app, - }, - position: { - x: serverX - COLUMN_SPACING, - y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20), - }, - style: { - background: "#f0f9ff", - color: "#0f0f0f", - border: "2px solid #60a5fa", - borderRadius: "4px", - padding: "6px", - width: APP_NODE_WIDTH, - height: APP_NODE_HEIGHT, - fontSize: "0.8rem", - lineHeight: "1.1", - whiteSpace: "pre-wrap", - }, - }); - }); - - // VMs (middle column) mit dynamischem Abstand - const hostVMs = servers.filter(vm => vm.hostServer === server.id); - let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING; - - hostVMs.forEach(vm => { - const appCount = applications.filter(app => app.serverId === vm.id).length; - - vmNodes.push({ - id: `vm-${vm.id}`, - type: "vm", - data: { - label: `${vm.name}\n${vm.ip}`, - ...vm, - }, - position: { - x: serverX, - y: currentY, - }, - style: { - background: "#fef2f2", - color: "#0f0f0f", - border: "2px solid #fecaca", - borderRadius: "4px", - padding: "6px", - width: APP_NODE_WIDTH, - height: APP_NODE_HEIGHT, - fontSize: "0.8rem", - lineHeight: "1.1", - whiteSpace: "pre-wrap", - }, - }); - - // Dynamischer Abstand basierend auf Anzahl Apps - const requiredSpace = appCount > 0 - ? (appCount * (APP_NODE_HEIGHT + APP_ROW_SPACING)) - : 0; - - currentY += Math.max( - requiredSpace + MIN_VM_SPACING, - MIN_VM_SPACING + APP_NODE_HEIGHT - ); - }); - } - }); - - // Level 4: VM Applications (right column) - const vmAppNodes: Node[] = []; - vmNodes.forEach((vm) => { - const vmX = vm.position.x; - applications - .filter(app => app.serverId === vm.data.id) - .forEach((app, appIndex) => { - vmAppNodes.push({ - id: `vm-app-${app.id}`, - type: "application", - data: { - label: `${app.name}\n${app.localURL}`, - ...app, - }, - position: { - x: vmX + VM_APP_SPACING, - y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20), - }, - style: { - background: "#f5f5f5", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "4px", - padding: "6px", - width: APP_NODE_WIDTH, - height: APP_NODE_HEIGHT, - fontSize: "0.8rem", - lineHeight: "1.1", - whiteSpace: "pre-wrap", - }, - }); - }); - }); - - // Calculate dimensions for root node positioning - const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes]; - let minX = Infinity; - let maxX = -Infinity; - let minY = Infinity; - let maxY = -Infinity; - - tempNodes.forEach((node) => { - const width = parseInt(node.style.width?.toString() || "0", 10); - const height = parseInt(node.style.height?.toString() || "0", 10); - - minX = Math.min(minX, node.position.x); - maxX = Math.max(maxX, node.position.x + width); - minY = Math.min(minY, node.position.y); - maxY = Math.max(maxY, node.position.y + height); - }); - - const centerX = (minX + maxX) / 2; - const rootX = centerX - ROOT_NODE_WIDTH / 2; - - // Level 1: Root Node (centered at top) - const rootNode: Node = { - id: "root", - type: "infrastructure", - data: { label: "My Infrastructure" }, - position: { x: rootX, y: 0 }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "8px", - padding: "16px", - width: ROOT_NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "1.2rem", - fontWeight: "bold", - }, - }; - - // Update dimensions with root node - const allNodes = [rootNode, ...tempNodes]; - let newMinX = Math.min(minX, rootNode.position.x); - let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH); - let newMinY = Math.min(minY, rootNode.position.y); - let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT); - - // Container Node - const containerNode: Node = { - id: 'container', - type: 'container', - data: { label: '' }, - position: { - x: newMinX - CONTAINER_PADDING, - y: newMinY - CONTAINER_PADDING - }, - style: { - width: newMaxX - newMinX + 2 * CONTAINER_PADDING, - height: newMaxY - newMinY + 2 * CONTAINER_PADDING, - background: 'transparent', - border: '2px dashed #e2e8f0', - borderRadius: '8px', - zIndex: 0, - }, - draggable: false, - selectable: false, - zIndex: -1, - }; - - // Connections with hierarchical chaining - const connections: Edge[] = []; - - // Root to Servers - serverNodes.forEach((server) => { - connections.push({ - id: `conn-root-${server.id}`, - source: "root", - target: server.id, - type: "straight", - style: { - stroke: "#94a3b8", - strokeWidth: 2, - }, - }); - }); - - // Services chaining - const servicesByServer = new Map(); - serviceNodes.forEach(service => { - const serverId = service.data.serverId; - if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []); - servicesByServer.get(serverId)!.push(service); - }); - servicesByServer.forEach((services, serverId) => { - services.sort((a, b) => a.position.y - b.position.y); - services.forEach((service, index) => { - if (index === 0) { - connections.push({ - id: `conn-service-${service.id}`, - source: `server-${serverId}`, - target: service.id, - type: "straight", - style: { stroke: "#60a5fa", strokeWidth: 2 }, - }); - } else { - const prevService = services[index - 1]; - connections.push({ - id: `conn-service-${service.id}-${prevService.id}`, - source: prevService.id, - target: service.id, - type: "straight", - style: { stroke: "#60a5fa", strokeWidth: 2 }, - }); - } - }); - }); - - // VMs chaining - const vmsByHost = new Map(); - vmNodes.forEach(vm => { - const hostId = vm.data.hostServer; - if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []); - vmsByHost.get(hostId)!.push(vm); - }); - vmsByHost.forEach((vms, hostId) => { - vms.sort((a, b) => a.position.y - b.position.y); - vms.forEach((vm, index) => { - if (index === 0) { - connections.push({ - id: `conn-vm-${vm.id}`, - source: `server-${hostId}`, - target: vm.id, - type: "straight", - style: { stroke: "#f87171", strokeWidth: 2 }, - }); - } else { - const prevVm = vms[index - 1]; - connections.push({ - id: `conn-vm-${vm.id}-${prevVm.id}`, - source: prevVm.id, - target: vm.id, - type: "straight", - style: { stroke: "#f87171", strokeWidth: 2 }, - }); - } - }); - }); - - // VM Applications chaining - const appsByVM = new Map(); - vmAppNodes.forEach(app => { - const vmId = app.data.serverId; - if (!appsByVM.has(vmId)) appsByVM.set(vmId, []); - appsByVM.get(vmId)!.push(app); - }); - appsByVM.forEach((apps, vmId) => { - apps.sort((a, b) => a.position.y - b.position.y); - apps.forEach((app, index) => { - if (index === 0) { - connections.push({ - id: `conn-vm-app-${app.id}`, - source: `vm-${vmId}`, - target: app.id, - type: "straight", - style: { stroke: "#f87171", strokeWidth: 2 }, - }); - } else { - const prevApp = apps[index - 1]; - connections.push({ - id: `conn-vm-app-${app.id}-${prevApp.id}`, - source: prevApp.id, - target: app.id, - type: "straight", - style: { stroke: "#f87171", strokeWidth: 2 }, - }); - } - }); - }); - - return NextResponse.json({ - nodes: [containerNode, ...allNodes], - edges: connections, - }); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - return NextResponse.json( - { - error: `Error fetching flowchart: ${errorMessage}`, - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/notifications/add/route.ts b/app/api/notifications/add/route.ts deleted file mode 100644 index db6a271..0000000 --- a/app/api/notifications/add/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface AddRequest { - type: string; - name: string; - smtpHost?: string; - smtpPort?: number; - smtpSecure?: boolean; - smtpUsername?: string; - smtpPassword?: string; - smtpFrom?: string; - smtpTo?: string; - telegramToken?: string; - telegramChatId?: string; - discordWebhook?: string; - gotifyUrl?: string; - gotifyToken?: string; - ntfyUrl?: string; - ntfyToken?: string; - pushoverUrl?: string; - pushoverToken?: string; - pushoverUser?: string; - echobellURL?: string; -} - -export async function POST(request: NextRequest) { - try { - const body: AddRequest = await request.json(); - const { type, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook, gotifyUrl, gotifyToken, ntfyUrl, ntfyToken, pushoverUrl, pushoverToken, pushoverUser, echobellURL } = body; - - const notification = await prisma.notification.create({ - data: { - type: type, - name: name, - smtpHost: smtpHost, - smtpPort: smtpPort, - smtpFrom: smtpFrom, - smtpUser: smtpUsername, - smtpPass: smtpPassword, - smtpSecure: smtpSecure, - smtpTo: smtpTo, - telegramChatId: telegramChatId, - telegramToken: telegramToken, - discordWebhook: discordWebhook, - gotifyUrl: gotifyUrl, - gotifyToken: gotifyToken, - ntfyUrl: ntfyUrl, - ntfyToken: ntfyToken, - pushoverUrl: pushoverUrl, - pushoverToken: pushoverToken, - pushoverUser: pushoverUser, - echobellURL: echobellURL - } - }); - - return NextResponse.json({ message: "Success", notification }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/notifications/delete/route.ts b/app/api/notifications/delete/route.ts deleted file mode 100644 index 67966a6..0000000 --- a/app/api/notifications/delete/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const id = Number(body.id); - - if (!id) { - return NextResponse.json({ error: "Missing ID" }, { status: 400 }); - } - - await prisma.notification.delete({ - where: { id: id } - }); - - return NextResponse.json({ success: true }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/notifications/get/route.ts b/app/api/notifications/get/route.ts deleted file mode 100644 index dff6e7f..0000000 --- a/app/api/notifications/get/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - - -export async function POST(request: NextRequest) { - try { - - const notifications = await prisma.notification.findMany(); - - return NextResponse.json({ - notifications - }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/notifications/test/route.ts b/app/api/notifications/test/route.ts deleted file mode 100644 index 200a5c6..0000000 --- a/app/api/notifications/test/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface AddRequest { - notificationId: number; -} - -export async function POST(request: NextRequest) { - try { - const body: AddRequest = await request.json(); - const { notificationId } = body; - - const notification = await prisma.test_notification.create({ - data: { - notificationId: notificationId, - } - }); - - return NextResponse.json({ message: "Success", notification }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/servers/add/route.ts b/app/api/servers/add/route.ts deleted file mode 100644 index 440d4d0..0000000 --- a/app/api/servers/add/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface AddRequest { - host: boolean; - hostServer: number; - name: string; - icon: string; - os: string; - ip: string; - url: string; - cpu: string; - gpu: string; - ram: string; - disk: string; - monitoring: boolean; - monitoringURL: string; - -} - -export async function POST(request: NextRequest) { - try { - const body: AddRequest = await request.json(); - const { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body; - - const server = await prisma.server.create({ - data: { - host, - hostServer, - name, - icon, - os, - ip, - url, - cpu, - gpu, - ram, - disk, - monitoring, - monitoringURL - } - }); - - return NextResponse.json({ message: "Success", server }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/servers/delete/route.ts b/app/api/servers/delete/route.ts deleted file mode 100644 index 9561f5e..0000000 --- a/app/api/servers/delete/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const id = Number(body.id); - - if (!id) { - return NextResponse.json({ error: "Missing ID" }, { status: 400 }); - } - - // Check if there are any applications associated with the server - const applications = await prisma.application.findMany({ - where: { serverId: id } - }); - if (applications.length > 0) { - return NextResponse.json({ error: "Cannot delete server with associated applications" }, { status: 400 }); - } - - // Delete all server history records for this server - await prisma.server_history.deleteMany({ - where: { serverId: id } - }); - - // Delete the server - await prisma.server.delete({ - where: { id: id } - }); - - return NextResponse.json({ success: true }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/servers/edit/route.ts b/app/api/servers/edit/route.ts deleted file mode 100644 index c857d68..0000000 --- a/app/api/servers/edit/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface EditRequest { - host: boolean; - hostServer: number; - id: number; - name: string; - icon: string; - os: string; - ip: string; - url: string; - cpu: string; - gpu: string; - ram: string; - disk: string; - monitoring: boolean; - monitoringURL: string; -} - -export async function PUT(request: NextRequest) { - try { - const body: EditRequest = await request.json(); - const { host, hostServer, id, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL } = body; - - const existingServer = await prisma.server.findUnique({ where: { id } }); - if (!existingServer) { - return NextResponse.json({ error: "Server not found" }, { status: 404 }); - } - - let newHostServer = hostServer; - if (hostServer === null) { - newHostServer = 0; - } else { - newHostServer = hostServer; - } - - const updatedServer = await prisma.server.update({ - where: { id }, - data: { - host, - hostServer: newHostServer, - name, - icon, - os, - ip, - url, - cpu, - gpu, - ram, - disk, - monitoring, - monitoringURL - } - }); - - return NextResponse.json({ message: "Server updated", server: updatedServer }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts deleted file mode 100644 index ac17d42..0000000 --- a/app/api/servers/get/route.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; -import { Prisma } from "@prisma/client"; - -interface GetRequest { - page?: number; - ITEMS_PER_PAGE?: number; - timeRange?: '1h' | '1d' | '7d' | '30d'; - serverId?: number; -} - -const getTimeRange = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => { - const now = new Date(); - switch (timeRange) { - case '1d': - return { - start: new Date(now.getTime() - 24 * 60 * 60 * 1000), - end: now, - intervalMinutes: 15 // 15 minute intervals - }; - case '7d': - return { - start: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), - end: now, - intervalMinutes: 60 // 1 hour intervals - }; - case '30d': - return { - start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), - end: now, - intervalMinutes: 240 // 4 hour intervals - }; - case '1h': - default: - return { - start: new Date(now.getTime() - 60 * 60 * 1000), - end: now, - intervalMinutes: 1 // 1 minute intervals - }; - } -}; - -const getIntervals = (timeRange: '1h' | '1d' | '7d' | '30d' = '1h') => { - const { start, end, intervalMinutes } = getTimeRange(timeRange); - - let intervalCount: number; - switch (timeRange) { - case '1d': - intervalCount = 96; // 24 hours * 4 (15-minute intervals) - break; - case '7d': - intervalCount = 168; // 7 days * 24 hours - break; - case '30d': - intervalCount = 180; // 30 days * 6 (4-hour intervals) - break; - case '1h': - default: - intervalCount = 60; - break; - } - - // Calculate the total time span in minutes - const totalMinutes = Math.floor((end.getTime() - start.getTime()) / (1000 * 60)); - - // Create equally spaced intervals - return Array.from({ length: intervalCount }, (_, i) => { - const minutesFromEnd = Math.floor(i * (totalMinutes / (intervalCount - 1))); - const d = new Date(end.getTime() - minutesFromEnd * 60 * 1000); - return d; - }).reverse(); // Return in chronological order -}; - -const parseUsageValue = (value: string | null): number => { - if (!value) return 0; - return Math.round(parseFloat(value.replace('%', '')) * 100) / 100; -}; - -export async function POST(request: NextRequest) { - try { - const body: GetRequest = await request.json(); - const page = Math.max(1, body.page || 1); - const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4; - const timeRange = body.timeRange || '1h'; - const serverId = body.serverId; - - // If serverId is provided, only fetch that specific server - const hostsQuery = serverId - ? { id: serverId } - : { hostServer: 0 }; - - let hosts; - if (!serverId) { - hosts = await prisma.server.findMany({ - where: hostsQuery, - orderBy: { name: 'asc' as Prisma.SortOrder }, - skip: (page - 1) * ITEMS_PER_PAGE, - take: ITEMS_PER_PAGE, - }); - } else { - hosts = await prisma.server.findMany({ - where: hostsQuery, - orderBy: { name: 'asc' as Prisma.SortOrder }, - }); - } - - const { start } = getTimeRange(timeRange); - const intervals = getIntervals(timeRange); - - const hostsWithVms = await Promise.all( - hosts.map(async (host) => { - const vms = await prisma.server.findMany({ - where: { hostServer: host.id }, - orderBy: { name: 'asc' } - }); - - // Get server history for the host - const serverHistory = await prisma.server_history.findMany({ - where: { - serverId: host.id, - createdAt: { - gte: start - } - }, - orderBy: { - createdAt: 'asc' - } - }); - - // Process history data into intervals - const historyMap = new Map(); - - // Initialize intervals - intervals.forEach(date => { - const key = date.toISOString(); - historyMap.set(key, { - cpu: [], - ram: [], - disk: [], - gpu: [], - temp: [], - online: [] - }); - }); - - // Group data by interval - serverHistory.forEach(record => { - const recordDate = new Date(record.createdAt); - let nearestInterval: Date = intervals[0]; - let minDiff = Infinity; - - // Find the nearest interval for this record - intervals.forEach(intervalDate => { - const diff = Math.abs(recordDate.getTime() - intervalDate.getTime()); - if (diff < minDiff) { - minDiff = diff; - nearestInterval = intervalDate; - } - }); - - const key = nearestInterval.toISOString(); - const interval = historyMap.get(key); - if (interval) { - interval.cpu.push(parseUsageValue(record.cpuUsage)); - interval.ram.push(parseUsageValue(record.ramUsage)); - interval.disk.push(parseUsageValue(record.diskUsage)); - interval.gpu.push(parseUsageValue(record.gpuUsage)); - interval.temp.push(parseUsageValue(record.temp)); - interval.online.push(record.online); - } - }); - - // Calculate averages for each interval - const historyData = intervals.map(date => { - const key = date.toISOString(); - const data = historyMap.get(key) || { - cpu: [], - ram: [], - disk: [], - gpu: [], - temp: [], - online: [] - }; - - const average = (arr: number[]) => - arr.length ? Math.round((arr.reduce((a, b) => a + b, 0) / arr.length) * 100) / 100 : null; - - return { - timestamp: key, - cpu: average(data.cpu), - ram: average(data.ram), - disk: average(data.disk), - gpu: average(data.gpu), - temp: average(data.temp), - online: data.online.length ? - data.online.filter(Boolean).length / data.online.length >= 0.5 - : null - }; - }); - - // Add isVM flag to VMs - const vmsWithFlag = vms.map(vm => ({ - ...vm, - isVM: true, - hostedVMs: [] // Initialize empty hostedVMs array for VMs - })); - - return { - ...host, - isVM: false, - hostedVMs: vmsWithFlag, - history: { - labels: intervals.map(d => d.toISOString()), - datasets: { - cpu: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.cpu || []; - return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; - }), - ram: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.ram || []; - return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; - }), - disk: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.disk || []; - return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; - }), - gpu: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.gpu || []; - return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; - }), - temp: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.temp || []; - return data.length ? Math.round((data.reduce((a, b) => a + b) / data.length) * 100) / 100 : null; - }), - online: intervals.map(d => { - const data = historyMap.get(d.toISOString())?.online || []; - return data.length ? data.filter(Boolean).length / data.length >= 0.5 : null; - }) - } - } - }; - }) - ); - - // Only calculate maxPage when not requesting a specific server - let maxPage = 1; - let totalHosts = 0; - if (!serverId) { - totalHosts = await prisma.server.count({ - where: { OR: [{ hostServer: 0 }, { hostServer: null }] } - }); - maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); - } - - return NextResponse.json({ - servers: hostsWithVms, - maxPage, - totalItems: totalHosts - }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/servers/hosts/route.ts b/app/api/servers/hosts/route.ts deleted file mode 100644 index 3d62621..0000000 --- a/app/api/servers/hosts/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -export async function GET(request: NextRequest) { - try { - const servers = await prisma.server.findMany({ - where: { host: true }, - }); - - // Add required properties to ensure consistency - const serversWithProps = servers.map(server => ({ - ...server, - isVM: false, - hostedVMs: [] // Initialize empty hostedVMs array - })); - - return NextResponse.json({ servers: serversWithProps }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/servers/monitoring/route.ts b/app/api/servers/monitoring/route.ts deleted file mode 100644 index 2a905d5..0000000 --- a/app/api/servers/monitoring/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextResponse } from "next/server" -import { prisma } from "@/lib/prisma"; - - -export async function GET() { - try { - const servers = await prisma.server.findMany({ - select: { - id: true, - online: true, - cpuUsage: true, - ramUsage: true, - diskUsage: true, - gpuUsage: true, - temp: true, - uptime: true - } - }); - - const monitoringData = servers.map((server: { - id: number; - online: boolean; - cpuUsage: string | null; - ramUsage: string | null; - diskUsage: string | null; - gpuUsage: string | null; - temp: string | null; - uptime: string | null; - }) => ({ - id: server.id, - online: server.online, - cpuUsage: server.cpuUsage ? parseFloat(server.cpuUsage) : 0, - ramUsage: server.ramUsage ? parseFloat(server.ramUsage) : 0, - diskUsage: server.diskUsage ? parseFloat(server.diskUsage) : 0, - gpuUsage: server.gpuUsage ? parseFloat(server.gpuUsage) : 0, - temp: server.temp ? parseFloat(server.temp) : 0, - uptime: server.uptime || "" - })); - - return NextResponse.json(monitoringData) - } catch (error) { - return new NextResponse("Internal Error", { status: 500 }) - } -} \ No newline at end of file diff --git a/app/api/servers/search/route.ts b/app/api/servers/search/route.ts deleted file mode 100644 index 9266919..0000000 --- a/app/api/servers/search/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; -import Fuse from "fuse.js"; - -interface SearchRequest { - searchterm: string; -} - -export async function POST(request: NextRequest) { - try { - const body: SearchRequest = await request.json(); - const { searchterm } = body; - - // Fetch all servers - const servers = await prisma.server.findMany({}); - - // Create a map of host servers with their hosted VMs - const serverMap = new Map(); - servers.forEach(server => { - if (server.host) { - serverMap.set(server.id, { - ...server, - isVM: false, - hostedVMs: [] - }); - } - }); - - // Add VMs to their host servers and mark them as VMs - const serversWithType = servers.map(server => { - // If not a host and has a hostServer, it's a VM - if (!server.host && server.hostServer) { - const hostServer = serverMap.get(server.hostServer); - if (hostServer) { - hostServer.hostedVMs.push({ - ...server, - isVM: true - }); - } - return { - ...server, - isVM: true - }; - } - return { - ...server, - isVM: false, - hostedVMs: serverMap.get(server.id)?.hostedVMs || [] - }; - }); - - const fuseOptions = { - keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk', 'os'], - threshold: 0.3, - includeScore: true, - }; - - const fuse = new Fuse(serversWithType, fuseOptions); - - const searchResults = fuse.search(searchterm); - - const results = searchResults.map(({ item }) => item); - - return NextResponse.json({ results }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/settings/get_notification_text/route.ts b/app/api/settings/get_notification_text/route.ts deleted file mode 100644 index 0c210e2..0000000 --- a/app/api/settings/get_notification_text/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - - -export async function POST(request: NextRequest) { - try { - // Check if there are any settings entries - const existingSettings = await prisma.settings.findFirst(); - if (!existingSettings) { - return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" }); - } - - // If settings entry exists, fetch it - const settings = await prisma.settings.findFirst({ - where: { id: existingSettings.id }, - }); - if (!settings) { - return NextResponse.json({ "notification_text_application": "", "notification_text_server": "" }); - } - // Return the settings entry - return NextResponse.json({ "notification_text_application": settings.notification_text_application, "notification_text_server": settings.notification_text_server }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/settings/notification_text/route.ts b/app/api/settings/notification_text/route.ts deleted file mode 100644 index 16ae26b..0000000 --- a/app/api/settings/notification_text/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { prisma } from "@/lib/prisma"; - -interface AddRequest { - text_application: string; - text_server: string; -} - -export async function POST(request: NextRequest) { - try { - const body: AddRequest = await request.json(); - const { text_application, text_server } = body; - - // Check if there is already a settings entry - const existingSettings = await prisma.settings.findFirst(); - if (existingSettings) { - // Update the existing settings entry - const updatedSettings = await prisma.settings.update({ - where: { id: existingSettings.id }, - data: { notification_text_application: text_application, notification_text_server: text_server }, - }); - return NextResponse.json({ message: "Success", updatedSettings }); - } - // If no settings entry exists, create a new one - const settings = await prisma.settings.create({ - data: { - notification_text_application: text_application, - notification_text_server: text_server, - } - }); - - return NextResponse.json({ message: "Success", settings }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/dashboard/Dashboard.tsx b/app/dashboard/Dashboard.tsx deleted file mode 100644 index f2b0e53..0000000 --- a/app/dashboard/Dashboard.tsx +++ /dev/null @@ -1,231 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import axios from "axios" -import Link from "next/link" -import { Activity, Layers, Network, Server } from "lucide-react" - -import { AppSidebar } from "@/components/app-sidebar" -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import { Separator } from "@/components/ui/separator" -import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { useTranslations } from "next-intl" - -interface StatsResponse { - serverCountNoVMs: number - serverCountOnlyVMs: number - applicationCount: number - onlineApplicationsCount: number -} - -export default function Dashboard() { - const t = useTranslations('Dashboard') - const [serverCountNoVMs, setServerCountNoVMs] = useState(0) - const [serverCountOnlyVMs, setServerCountOnlyVMs] = useState(0) - const [applicationCount, setApplicationCount] = useState(0) - const [onlineApplicationsCount, setOnlineApplicationsCount] = useState(0) - - const getStats = async () => { - try { - const response = await axios.post("/api/dashboard/get", {}) - setServerCountNoVMs(response.data.serverCountNoVMs) - setServerCountOnlyVMs(response.data.serverCountOnlyVMs) - setApplicationCount(response.data.applicationCount) - setOnlineApplicationsCount(response.data.onlineApplicationsCount) - } catch (error: any) { - console.log("Axios error:", error.response?.data) - } - } - - useEffect(() => { - getStats() - }, []) - - return ( - - - -
-
- - - - - - / - - - - {t('Title')} - - - -
-
-
-

{t('Title')}

- -
- - -
-
- {t('Servers.Title')} - {t('Servers.Description')} -
- -
-
- -
- {/* Physical Servers */} -
-
- -
-
-
{serverCountNoVMs}
-

{t('Servers.PhysicalServers')}

-
-
- - {/* Virtual Machines */} -
-
- -
-
-
{serverCountOnlyVMs}
-

{t('Servers.VirtualServers')}

-
-
-
-
- - - -
- - - -
-
- {t('Applications.Title')} - {t('Applications.Description')} -
- -
-
- -
{applicationCount}
-

{t('Applications.OnlineApplications')}

-
- - - -
- - - -
-
- {t('Uptime.Title')} - {t('Uptime.Description')} -
- -
-
- -
-
- - {onlineApplicationsCount}/{applicationCount} - -
- {applicationCount > 0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}% -
-
-
-
0 ? Math.round((onlineApplicationsCount / applicationCount) * 100) : 0}%`, - }} - >
-
-

{t('Uptime.OnlineApplications')}

-
-
- - - -
- - - -
-
- {t('Network.Title')} - {t('Network.Description')} -
- -
-
- -
{serverCountNoVMs + serverCountOnlyVMs + applicationCount}
-

{t('Network.ActiveConnections')}

-
- - - -
-
-
-
-
- ) -} diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx deleted file mode 100644 index 5b27a54..0000000 --- a/app/dashboard/applications/Applications.tsx +++ /dev/null @@ -1,1172 +0,0 @@ -"use client"; - -import { AppSidebar } from "@/components/app-sidebar"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { Separator } from "@/components/ui/separator"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; -import { Button } from "@/components/ui/button"; -import { - Plus, - Link, - Home, - Trash2, - LayoutGrid, - List, - Pencil, - Zap, - ViewIcon, - Grid3X3, - HelpCircle, -} from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Pagination, - PaginationContent, - PaginationEllipsis, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import Cookies from "js-cookie"; -import { useState, useEffect, useRef } from "react"; -import axios from "axios"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { StatusIndicator } from "@/components/status-indicator"; -import { Toaster } from "@/components/ui/sonner" -import { toast } from "sonner" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useTranslations } from "next-intl"; - -interface Application { - id: number; - name: string; - description?: string; - icon?: string; - publicURL: string; - localURL?: string; - server?: string; - online: boolean; - serverId: number; - uptimecheckUrl?: string; -} - -interface Server { - id: number; - name: string; -} - -interface ApplicationsResponse { - applications: Application[]; - servers: Server[]; - maxPage: number; - totalItems?: number; -} - -export default function Dashboard() { - const t = useTranslations(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [icon, setIcon] = useState(""); - const [publicURL, setPublicURL] = useState(""); - const [localURL, setLocalURL] = useState(""); - const [serverId, setServerId] = useState(null); - const [customUptimeCheck, setCustomUptimeCheck] = useState(false); - const [uptimecheckUrl, setUptimecheckUrl] = useState(""); - - const [editName, setEditName] = useState(""); - const [editDescription, setEditDescription] = useState(""); - const [editIcon, setEditIcon] = useState(""); - const [editPublicURL, setEditPublicURL] = useState(""); - const [editLocalURL, setEditLocalURL] = useState(""); - const [editId, setEditId] = useState(null); - const [editServerId, setEditServerId] = useState(null); - const [editCustomUptimeCheck, setEditCustomUptimeCheck] = useState(false); - const [editUptimecheckUrl, setEditUptimecheckUrl] = useState(""); - - const [currentPage, setCurrentPage] = useState(1); - const [maxPage, setMaxPage] = useState(1); - const [applications, setApplications] = useState([]); - const [servers, setServers] = useState([]); - const [loading, setLoading] = useState(true); - - const [searchTerm, setSearchTerm] = useState(""); - const [isSearching, setIsSearching] = useState(false); - - const savedLayout = Cookies.get("layoutPreference-app"); - const savedItemsPerPage = Cookies.get("itemsPerPage-app"); - const initialIsGridLayout = savedLayout === "grid"; - const initialIsCompactLayout = savedLayout === "compact"; - const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5); - const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; - - const [isGridLayout, setIsGridLayout] = useState(initialIsGridLayout); - const [isCompactLayout, setIsCompactLayout] = useState(initialIsCompactLayout); - const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); - const customInputRef = useRef(null); - const debounceTimerRef = useRef(null); - - const toggleLayout = (layout: string) => { - if (layout === "standard") { - setIsGridLayout(false); - setIsCompactLayout(false); - Cookies.set("layoutPreference-app", "standard", { - expires: 365, - path: "/", - sameSite: "strict", - }); - } else if (layout === "grid") { - setIsGridLayout(true); - setIsCompactLayout(false); - Cookies.set("layoutPreference-app", "grid", { - expires: 365, - path: "/", - sameSite: "strict", - }); - } else if (layout === "compact") { - setIsGridLayout(false); - setIsCompactLayout(true); - Cookies.set("layoutPreference-app", "compact", { - expires: 365, - path: "/", - sameSite: "strict", - }); - } - }; - - const handleItemsPerPageChange = (value: string) => { - 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 () => { - try { - await axios.post("/api/applications/add", { - name, - description, - icon, - publicURL, - localURL, - serverId, - uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "", - }); - getApplications(); - toast.success(t('Applications.Messages.AddSuccess')); - } catch (error: any) { - console.log(error.response?.data); - toast.error(t('Applications.Messages.AddError')); - } - }; - - const getApplications = async () => { - try { - setLoading(true); - const response = await axios.post( - "/api/applications/get", - { page: currentPage, ITEMS_PER_PAGE: itemsPerPage } - ); - setApplications(response.data.applications); - setServers(response.data.servers); - setMaxPage(response.data.maxPage); - if (response.data.totalItems !== undefined) { - setTotalItems(response.data.totalItems); - } - setLoading(false); - } catch (error: any) { - console.log(error.response?.data); - toast.error(t('Applications.Messages.GetError')); - } - }; - - // Calculate current range of items being displayed - const [totalItems, setTotalItems] = useState(0); - const startItem = (currentPage - 1) * itemsPerPage + 1; - const endItem = Math.min(currentPage * itemsPerPage, totalItems); - - useEffect(() => { - getApplications(); - }, [currentPage, itemsPerPage]); - - const handlePrevious = () => setCurrentPage((prev) => Math.max(1, prev - 1)); - const handleNext = () => - setCurrentPage((prev) => Math.min(maxPage, prev + 1)); - - const deleteApplication = async (id: number) => { - try { - await axios.post("/api/applications/delete", { id }); - getApplications(); - toast.success(t('Applications.Messages.DeleteSuccess')); - } catch (error: any) { - console.log(error.response?.data); - toast.error(t('Applications.Messages.DeleteError')); - } - }; - - const openEditDialog = (app: Application) => { - setEditId(app.id); - setEditServerId(app.serverId); - setEditName(app.name); - setEditDescription(app.description || ""); - setEditIcon(app.icon || ""); - setEditLocalURL(app.localURL || ""); - setEditPublicURL(app.publicURL || ""); - - if (app.uptimecheckUrl) { - setEditCustomUptimeCheck(true); - setEditUptimecheckUrl(app.uptimecheckUrl); - } else { - setEditCustomUptimeCheck(false); - setEditUptimecheckUrl(""); - } - }; - - const edit = async () => { - if (!editId) return; - - try { - await axios.put("/api/applications/edit", { - id: editId, - serverId: editServerId, - name: editName, - description: editDescription, - icon: editIcon, - publicURL: editPublicURL, - localURL: editLocalURL, - uptimecheckUrl: editCustomUptimeCheck ? editUptimecheckUrl : "", - }); - getApplications(); - setEditId(null); - toast.success(t('Applications.Messages.EditSuccess')); - } catch (error: any) { - console.log(error.response.data); - toast.error(t('Applications.Messages.EditError')); - } - }; - - const searchApplications = async () => { - try { - setIsSearching(true); - const response = await axios.post<{ results: Application[] }>( - "/api/applications/search", - { searchterm: searchTerm } - ); - setApplications(response.data.results); - setIsSearching(false); - } catch (error: any) { - console.error("Search error:", error.response?.data); - setIsSearching(false); - } - }; - - useEffect(() => { - const delayDebounce = setTimeout(() => { - if (searchTerm.trim() === "") { - getApplications(); - } else { - searchApplications(); - } - }, 300); - - return () => clearTimeout(delayDebounce); - }, [searchTerm]); - - const generateIconURL = async () => { - setIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + name.toLowerCase() + ".png") - } - - const generateEditIconURL = async () => { - setEditIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + editName.toLowerCase() + ".png") - } - - return ( - - - -
-
- - - - - - / - - - - {t('Applications.Breadcrumb.MyInfrastructure')} - - - - {t('Applications.Breadcrumb.Applications')} - - - -
-
- -
-
- {t('Applications.Title')} -
- - - - - - toggleLayout("standard")}> - {t('Applications.Views.ListView')} - - toggleLayout("grid")}> - {t('Applications.Views.GridView')} - - toggleLayout("compact")}> - {t('Applications.Views.CompactView')} - - - - { - // 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()} - /> - items -
-
- - - {servers.length === 0 ? ( -

- {t('Applications.Messages.AddServerFirst')} -

- ) : ( - - - - - - - {t('Applications.Add.Title')} - -
-
- - setName(e.target.value)} - /> -
-
- - -
-
- -