diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 2647363..e4f6115 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,6 +1,6 @@
# These are supported funding model platforms
-github: #[tolaleng]
+github: [tolaleng]
patreon: # Replace with a single Patreon username
open_collective: #checkcle
ko_fi: # Replace with a single Ko-fi username
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..2fa2743
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,93 @@
+name: π Bug Report
+description: Report a reproducible bug to help us improve.
+title: "[Bug]: "
+labels: [bug]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Thank you for taking the time to report a bug!**
+ Please complete the form below to help us reproduce and fix the issue more effectively.
+
+ - type: input
+ id: environment
+ attributes:
+ label: Environment
+ description: |
+ Describe the environment where the issue occurred.
+ (e.g., OS, browser, Node.js version, Docker version, etc.)
+ placeholder: "Example: Ubuntu 22.04, Chrome 125, Node.js 20.3.0"
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Bug Description
+ description: |
+ A clear and concise description of the issue you encountered.
+ placeholder: "When I do X, Y happens instead of Z..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to Reproduce
+ description: |
+ Provide step-by-step instructions to reproduce the issue.
+ placeholder: |
+ 1. Go to '...'
+ 2. Click on '...'
+ 3. Scroll down to '...'
+ 4. See the error
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ description: What did you expect to happen?
+ placeholder: "The app should..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots or Videos
+ description: |
+ If applicable, add screenshots or screen recordings to help illustrate the issue.
+ placeholder: "Drag and drop or paste screenshots here."
+ validations:
+ required: false
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs
+ description: |
+ If applicable, paste any relevant logs or error messages.
+ Please remove sensitive information before submitting.
+ render: shell
+ placeholder: |
+ ```
+ Error: Something went wrong
+ at index.js:123:45
+ ```
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Checklist
+ description: Please confirm the following before submitting:
+ options:
+ - label: I have searched existing issues to avoid creating duplicates.
+ required: true
+ - label: I have provided sufficient information to reproduce the issue.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..a49eab2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml
new file mode 100644
index 0000000..735acc2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.yml
@@ -0,0 +1,49 @@
+name: π Documentation Improvement
+description: Suggest improvements or report issues in documentation.
+title: "[Docs]: "
+labels: [documentation]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for helping improve our documentation!
+
+ - type: textarea
+ id: description
+ attributes:
+ label: What part of the documentation needs improvement?
+ description: |
+ Describe the issue clearly.
+ placeholder: "The installation guide is missing steps for..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: location
+ attributes:
+ label: Location
+ description: |
+ Provide a link or path to the affected documentation.
+ placeholder: "https://github.com/operacle/checkcle/docs/INSTALL.md"
+ validations:
+ required: true
+
+ - type: textarea
+ id: suggestion
+ attributes:
+ label: Suggested Change
+ description: |
+ How would you improve it?
+ placeholder: "I suggest adding a section about..."
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues for similar documentation problems.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..5c9b4e6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,59 @@
+name: π Feature Request
+description: Suggest an idea to improve this project.
+title: "[Feature]: "
+labels: [enhancement]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Thank you for suggesting a feature!**
+ Please fill out this form so we can better understand your idea.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Feature Description
+ description: A clear and concise description of the feature youβd like to see.
+ placeholder: "I would like to have..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: motivation
+ attributes:
+ label: Motivation
+ description: Please explain why this feature would be useful.
+ placeholder: "This feature would help because..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered
+ description: Have you considered any alternative solutions or workarounds?
+ placeholder: "I have tried..."
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Add any other context or screenshots about the feature request here.
+ placeholder: "Links to related issues or references..."
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ description: Before submitting, please confirm:
+ options:
+ - label: I have searched existing issues to make sure this feature hasnβt been requested yet.
+ required: true
+ - label: I have described the feature clearly and provided supporting details.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 0000000..4b20107
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,53 @@
+name: β Ask for Help
+description: Ask a question or request guidance related to this project.
+title: "[Question]: "
+labels: [question]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Thanks for reaching out!**
+ Please provide as much detail as possible so we can better assist you.
+
+ - type: textarea
+ id: question
+ attributes:
+ label: What do you need help with?
+ description: Clearly describe your question or what you're trying to achieve.
+ placeholder: |
+ I am trying to do X, but I'm not sure how to...
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: |
+ Provide any relevant context, related issues, pull requests, or documentation links that could help us understand your question better.
+ placeholder: "Mention related issues, links to docs, or previous discussions..."
+ validations:
+ required: false
+
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment Details
+ description: |
+ If relevant, provide details about your environment (e.g., OS, browser, Node.js version).
+ placeholder: "Example: Windows 11, Node.js v20.3.0, Firefox 125"
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ description: Before submitting, please confirm the following:
+ options:
+ - label: I have searched existing issues and discussions for a solution.
+ required: true
+ - label: I have read the relevant documentation.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/security_issue.yml b/.github/ISSUE_TEMPLATE/security_issue.yml
new file mode 100644
index 0000000..31e140c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/security_issue.yml
@@ -0,0 +1,51 @@
+name: π‘οΈ Security Issue
+description: Report a potential security vulnerability.
+title: "[Security]: "
+labels: [security]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ β οΈ **IMPORTANT: Please do NOT disclose sensitive security details in this public issue.**
+
+ If you believe you have found a security vulnerability, **please report it privately** to help keep users safe.
+
+ π§ **Disclosure Process:**
+ - Email: [security@checkcle.io](mailto:security@checkcle.io)
+ - Or follow our [SECURITY.md](../../blob/main/SECURITY.md) policy.
+
+ You can use this issue only to let maintainers know that you have sent or will send a report.
+
+ - type: textarea
+ id: summary
+ attributes:
+ label: Summary
+ description: |
+ Briefly describe the type of vulnerability you believe you have found (without including sensitive details).
+ placeholder: "Example: Possible SQL injection in the user login endpoint."
+ validations:
+ required: true
+
+ - type: textarea
+ id: contact
+ attributes:
+ label: Contact Information
+ description: |
+ Provide an email or other way for maintainers to contact you if we need clarification.
+ placeholder: "your.email@example.com"
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: Confirmation
+ description: |
+ Please confirm:
+ options:
+ - label: I will not share sensitive exploit details in this issue.
+ required: true
+ - label: I have sent or will send a full report to the private disclosure contact.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/translation_request.yml b/.github/ISSUE_TEMPLATE/translation_request.yml
new file mode 100644
index 0000000..a156fca
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/translation_request.yml
@@ -0,0 +1,38 @@
+name: π Translation Request
+description: Request a translation or report translation issues.
+title: "[Translation]: "
+labels: [translation]
+assignees: []
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Help us make this project accessible in more languages!
+
+ - type: input
+ id: language
+ attributes:
+ label: Language
+ description: What language is this about?
+ placeholder: "Example: Spanish"
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Details
+ description: |
+ Describe what youβd like to have translated or what issue you found.
+ placeholder: "Please translate the Getting Started guide..."
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues for similar requests.
+ required: true
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..b0ca797
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,81 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders of the **CheckCle** project pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, 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:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances of any kind
+- Trolling, insulting or 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
+
+## Enforcement Responsibilities
+
+Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+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, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and also applies when an individual is officially representing the project in public spaces. Examples include using an official project email 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 to the project team at:
+
+π§ **Email:** [hello@checkcle.io](mailto:hello@checkcle.io)
+
+All complaints will be reviewed and investigated promptly and fairly.
+
+All project maintainers are obligated to respect the privacy and security of the reporter of any incident.
+
+## Enforcement Guidelines
+
+Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+
+**1. Correction**
+
+- *Community Impact:* Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+- *Consequence:* A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+
+**2. Warning**
+
+- *Community Impact:* A violation through a single incident or series of actions.
+- *Consequence:* A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+
+**3. Temporary Ban**
+
+- *Community Impact:* A serious violation of community standards, including sustained inappropriate behavior.
+- *Consequence:* A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+
+**4. Permanent Ban**
+
+- *Community Impact:* Demonstrating a pattern of violation of community standards, including sustained harassment, or showing no remorse after being warned.
+- *Consequence:* A permanent ban from any sort of public interaction within the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at:
+
+https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+
+For answers to common questions about this code of conduct, see:
+
+https://www.contributor-covenant.org/faq
+
+[homepage]: https://www.contributor-covenant.org
diff --git a/README.md b/README.md
index 2bdff25..d9cbe4f 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
## π― Live Demo
π **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io)
+ User: admin@example.com | Passwd: Admin123456
## π Core Features
@@ -110,11 +111,12 @@ services:
4. Follow the Quick Start Guide at https://docs.checkcle.io
###
-
-
-
+
+
+
+

-
+
## π Development Roadmap
@@ -128,10 +130,10 @@ services:
- β
SSL & Domain Monitoring
- β
Schedule Maintenance
- β
Incident Management
-- [ ] Infrastructure Server Monitoring
+- β
Infrastructure Server Monitoring
- β
Operational Status / Public Status Pages
- β
Uptime monitoring (HTTP, TCP, PING, DNS) Full functionality
-- β
Distributed Regional Monitoring Agent [Support Operation](https://github.com/operacle/Distributed-Regional-Monitoring)
+- β
Distributed Regional Monitoring Agent
- β
System Setting Panel and Mail Settings
- β
User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
@@ -161,7 +163,7 @@ Here are some ways you can help improve CheckCle:
- Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
- GitHub Repository: β [CheckCle](https://github.com/operacle/checkcle.git)
-- Community Channels: Engage via discussions and issues!
+- Reddit Community: [reddit](https://www.reddit.com/r/checkcle)
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
@@ -171,12 +173,7 @@ CheckCle is released under the MIT License.
---
## π₯ Contributors
+Thank you for contributing and continuously making CheckCle better, you're awesome π«Ά
[](https://github.com/operacle/checkcle/graphs/contributors)
-
-## Star History
-
-[](https://www.star-history.com/#operacle/checkcle&Date)
-
-Stay informed, stay online, with CheckCle! π
diff --git a/application/public/favicon.ico b/application/public/favicon.ico
index 7ae5c72..d5dfc62 100644
Binary files a/application/public/favicon.ico and b/application/public/favicon.ico differ
diff --git a/application/public/favicon_sidebar.ico b/application/public/favicon_sidebar.ico
new file mode 100644
index 0000000..074b8f9
Binary files /dev/null and b/application/public/favicon_sidebar.ico differ
diff --git a/application/public/upload/os/centos.png b/application/public/upload/os/centos.png
new file mode 100644
index 0000000..a946ebc
Binary files /dev/null and b/application/public/upload/os/centos.png differ
diff --git a/application/public/upload/os/debian.png b/application/public/upload/os/debian.png
new file mode 100644
index 0000000..9b025dc
Binary files /dev/null and b/application/public/upload/os/debian.png differ
diff --git a/application/public/upload/os/linux.png b/application/public/upload/os/linux.png
new file mode 100644
index 0000000..8a69099
Binary files /dev/null and b/application/public/upload/os/linux.png differ
diff --git a/application/public/upload/os/rhel.png b/application/public/upload/os/rhel.png
new file mode 100644
index 0000000..5fd5e8b
Binary files /dev/null and b/application/public/upload/os/rhel.png differ
diff --git a/application/public/upload/os/ubuntu.png b/application/public/upload/os/ubuntu.png
new file mode 100644
index 0000000..fe086e2
Binary files /dev/null and b/application/public/upload/os/ubuntu.png differ
diff --git a/application/public/upload/os/windows.png b/application/public/upload/os/windows.png
new file mode 100644
index 0000000..cefd4ae
Binary files /dev/null and b/application/public/upload/os/windows.png differ
diff --git a/application/src/api/realtime/index.ts b/application/src/api/realtime/index.ts
index 3a53ee0..e114a68 100644
--- a/application/src/api/realtime/index.ts
+++ b/application/src/api/realtime/index.ts
@@ -2,7 +2,7 @@
// This file handles realtime notifications in a client-side environment
// In a production app, this would be a server-side endpoint
-console.log("API Realtime endpoint loaded");
+//console.log("API Realtime endpoint loaded");
// Simple implementation that simulates sending notifications
export default async function handler(req) {
diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx
index 4d15f9d..21ba733 100644
--- a/application/src/components/dashboard/Sidebar.tsx
+++ b/application/src/components/dashboard/Sidebar.tsx
@@ -13,7 +13,13 @@ export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme();
return (
-
+
diff --git a/application/src/components/dashboard/sidebar/MenuItem.tsx b/application/src/components/dashboard/sidebar/MenuItem.tsx
index 019d5ad..ec9f8cd 100644
--- a/application/src/components/dashboard/sidebar/MenuItem.tsx
+++ b/application/src/components/dashboard/sidebar/MenuItem.tsx
@@ -34,7 +34,6 @@ export const MenuItem: React.FC
= ({
e.stopPropagation();
if (hasNavigation && path) {
- // Use navigate instead of window.location to prevent full page reload
navigate(path, { replace: false });
}
};
@@ -44,11 +43,22 @@ export const MenuItem: React.FC = ({
return (
- {!collapsed && {t(translationKey)} }
+ {!collapsed && (
+
+ {t(translationKey)}
+
+ )}
);
};
\ No newline at end of file
diff --git a/application/src/components/dashboard/sidebar/SidebarHeader.tsx b/application/src/components/dashboard/sidebar/SidebarHeader.tsx
index 4ba177c..0ebbe2b 100644
--- a/application/src/components/dashboard/sidebar/SidebarHeader.tsx
+++ b/application/src/components/dashboard/sidebar/SidebarHeader.tsx
@@ -13,7 +13,7 @@ export const SidebarHeader: React.FC = ({ collapsed }) => {
diff --git a/application/src/components/docker/DockerContainersTable.tsx b/application/src/components/docker/DockerContainersTable.tsx
index 5ec906c..9e39429 100644
--- a/application/src/components/docker/DockerContainersTable.tsx
+++ b/application/src/components/docker/DockerContainersTable.tsx
@@ -62,7 +62,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
-
+
diff --git a/application/src/components/docker/DockerStatsCards.tsx b/application/src/components/docker/DockerStatsCards.tsx
index 6376b6d..cf43d56 100644
--- a/application/src/components/docker/DockerStatsCards.tsx
+++ b/application/src/components/docker/DockerStatsCards.tsx
@@ -3,44 +3,51 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Container, Play, Square, AlertTriangle } from "lucide-react";
import { DockerStats } from "@/types/docker.types";
+import { useTheme } from "@/contexts/ThemeContext";
interface DockerStatsCardsProps {
stats: DockerStats;
}
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
+ const { theme } = useTheme();
+
const cards = [
{
title: "Total Containers",
value: stats.total,
icon: Container,
color: "text-blue-600",
- bgColor: "bg-blue-50",
- borderColor: "border-blue-200",
+ gradient: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(59, 130, 246, 0.6) 100%)"
+ : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
},
{
title: "Running",
value: stats.running,
icon: Play,
color: "text-green-600",
- bgColor: "bg-green-50",
- borderColor: "border-green-200",
+ gradient: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(16, 185, 129, 0.6) 100%)"
+ : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
},
{
title: "Stopped",
value: stats.stopped,
icon: Square,
color: "text-gray-600",
- bgColor: "bg-gray-50",
- borderColor: "border-gray-200",
+ gradient: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(107, 114, 128, 0.6) 100%)"
+ : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
},
{
title: "Warning",
value: stats.warning,
icon: AlertTriangle,
color: "text-amber-600",
- bgColor: "bg-amber-50",
- borderColor: "border-amber-200",
+ gradient: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(245, 158, 11, 0.6) 100%)"
+ : "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #f59e0b 100%)"
},
];
@@ -49,23 +56,39 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
{cards.map((card) => {
const IconComponent = card.icon;
return (
-
-
-
+
+ {/* Grid Pattern Overlay */}
+
+
+
+
{card.title}
-
-
+
+
-
+
-
+
{card.value}
Containers
diff --git a/application/src/components/operational-page/ComponentsSelector.tsx b/application/src/components/operational-page/ComponentsSelector.tsx
index e291a9f..9478fc7 100644
--- a/application/src/components/operational-page/ComponentsSelector.tsx
+++ b/application/src/components/operational-page/ComponentsSelector.tsx
@@ -1,3 +1,4 @@
+
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -6,7 +7,7 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
-import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react';
+import { Plus, X, Server } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
@@ -17,12 +18,6 @@ interface ComponentsSelectorProps {
onComponentDelete?: (componentId: string) => void;
}
-const componentTypes = [
- { value: 'uptime', label: 'Uptime Service', icon: Server },
- { value: 'ssl', label: 'SSL Certificate', icon: Shield },
- { value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle },
-];
-
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
@@ -169,7 +164,7 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
-
+
{services.map((service) => (
@@ -206,37 +201,6 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
)}
-
-
-
Quick Add Templates
-
- {componentTypes.map((type) => {
- const Icon = type.icon;
- return (
- {
- const component: Partial = {
- name: type.label,
- description: `Monitor ${type.label.toLowerCase()}`,
- service_id: '',
- server_id: '',
- display_order: selectedComponents.length + 1,
- operational_status_id: '',
- };
- onComponentsChange([...selectedComponents, component]);
- }}
- className="justify-start"
- >
-
- {type.label}
-
- );
- })}
-
-
);
diff --git a/application/src/components/operational-page/CreateOperationalPageDialog.tsx b/application/src/components/operational-page/CreateOperationalPageDialog.tsx
index 4f9f17c..4d3c321 100644
--- a/application/src/components/operational-page/CreateOperationalPageDialog.tsx
+++ b/application/src/components/operational-page/CreateOperationalPageDialog.tsx
@@ -67,13 +67,13 @@ export const CreateOperationalPageDialog = () => {
page_style: data.page_style || '',
};
- console.log('Creating operational page with payload:', payload);
+ // console.log('Creating operational page with payload:', payload);
const createdPage = await createMutation.mutateAsync(payload);
- console.log('Created page:', createdPage);
+ // console.log('Created page:', createdPage);
// Create components after page is created
if (selectedComponents.length > 0) {
- console.log('Creating components for page:', createdPage.id);
+ // console.log('Creating components for page:', createdPage.id);
for (const component of selectedComponents) {
const componentPayload = {
operational_status_id: createdPage.id,
@@ -84,7 +84,7 @@ export const CreateOperationalPageDialog = () => {
display_order: component.display_order || 1,
};
- console.log('Creating component with payload:', componentPayload);
+ // console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
}
@@ -93,7 +93,7 @@ export const CreateOperationalPageDialog = () => {
form.reset();
setSelectedComponents([]);
} catch (error) {
- console.error('Error creating operational page:', error);
+ // console.error('Error creating operational page:', error);
}
};
diff --git a/application/src/components/operational-page/OperationalPageCard.tsx b/application/src/components/operational-page/OperationalPageCard.tsx
index 8c8949d..8c11dfd 100644
--- a/application/src/components/operational-page/OperationalPageCard.tsx
+++ b/application/src/components/operational-page/OperationalPageCard.tsx
@@ -41,11 +41,11 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
Public:
-
+
{page.is_public === 'true' ? 'Yes' : 'No'}
-
+
Updated:
diff --git a/application/src/components/public/PublicStatusPage.tsx b/application/src/components/public/PublicStatusPage.tsx
index 8f775f0..d6f7bc5 100644
--- a/application/src/components/public/PublicStatusPage.tsx
+++ b/application/src/components/public/PublicStatusPage.tsx
@@ -12,7 +12,7 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
export const PublicStatusPage = () => {
const { slug } = useParams<{ slug: string }>();
- console.log('PublicStatusPage - slug from params:', slug);
+// console.log('PublicStatusPage - slug from params:', slug);
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
const [lastUpdated, setLastUpdated] = useState(new Date());
@@ -51,7 +51,7 @@ export const PublicStatusPage = () => {
};
}, [page?.theme]);
- console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
+ // console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
if (loading) {
return (
diff --git a/application/src/components/public/hooks/usePublicStatusPageData.ts b/application/src/components/public/hooks/usePublicStatusPageData.ts
index 75983ac..a92e85b 100644
--- a/application/src/components/public/hooks/usePublicStatusPageData.ts
+++ b/application/src/components/public/hooks/usePublicStatusPageData.ts
@@ -19,61 +19,61 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
useEffect(() => {
const fetchPublicPage = async () => {
if (!slug) {
- console.log('No slug provided');
+ // console.log('No slug provided');
setError('No status page slug provided');
setLoading(false);
return;
}
try {
- console.log('Fetching public status page for slug:', slug);
+ // console.log('Fetching public status page for slug:', slug);
setLoading(true);
setError(null);
// Fetch operational page
- console.log('Fetching operational pages...');
+ // console.log('Fetching operational pages...');
const pages = await operationalPageService.getOperationalPages();
- console.log('All pages:', pages);
+ // console.log('All pages:', pages);
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
- console.log('Found page:', foundPage);
+ // console.log('Found page:', foundPage);
if (!foundPage) {
- console.log('Page not found or not public');
+ // console.log('Page not found or not public');
setError('Status page not found or not public');
setLoading(false);
return;
}
setPage(foundPage);
- console.log('Page set successfully');
+ // console.log('Page set successfully');
// Fetch components for this page
- console.log('Fetching components for page:', foundPage.id);
+ // console.log('Fetching components for page:', foundPage.id);
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
- console.log('Components found:', pageComponents);
+ // console.log('Components found:', pageComponents);
setComponents(pageComponents);
// Fetch all services
- console.log('Fetching all services...');
+ // console.log('Fetching all services...');
const allServices = await serviceService.getServices();
- console.log('Services found:', allServices);
+ // console.log('Services found:', allServices);
setServices(allServices);
// Fetch uptime data for each component that has a service
- console.log('Fetching uptime data...');
+ // console.log('Fetching uptime data...');
const uptimePromises = pageComponents
.filter(component => component.service_id)
.map(async (component) => {
try {
- console.log('Fetching uptime for service:', component.service_id);
+ // console.log('Fetching uptime for service:', component.service_id);
const endDate = new Date();
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
- console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
+ // console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
return { serviceId: component.service_id, history };
} catch (error) {
- console.error(`Error fetching uptime for service ${component.service_id}:`, error);
+ // console.error(`Error fetching uptime for service ${component.service_id}:`, error);
return { serviceId: component.service_id, history: [] };
}
});
@@ -84,12 +84,12 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
uptimeMap[result.serviceId] = result.history;
});
setUptimeData(uptimeMap);
- console.log('Uptime data set successfully');
+ // console.log('Uptime data set successfully');
- console.log('All data fetched successfully');
+ // console.log('All data fetched successfully');
} catch (err) {
- console.error('Error fetching public page:', err);
+ // console.error('Error fetching public page:', err);
setError(`Failed to load status page: ${err}`);
} finally {
setLoading(false);
diff --git a/application/src/components/schedule-incident/common/OverviewCard.tsx b/application/src/components/schedule-incident/common/OverviewCard.tsx
index 6c96010..ccbeb45 100644
--- a/application/src/components/schedule-incident/common/OverviewCard.tsx
+++ b/application/src/components/schedule-incident/common/OverviewCard.tsx
@@ -18,6 +18,7 @@ interface OverviewCardProps {
valueClassName?: string;
isLoading?: boolean;
color?: string;
+ gradient?: string;
}
export const OverviewCard = ({
@@ -30,11 +31,15 @@ export const OverviewCard = ({
valueClassName,
isLoading = false,
color = "blue",
+ gradient,
}: OverviewCardProps) => {
const { theme } = useTheme();
// Map color prop to gradient colors
const getGradientBackground = () => {
+ if (gradient) {
+ return gradient;
+ }
const colors = {
blue: theme === 'dark'
? "linear-gradient(135deg, rgba(25, 118, 210, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)"
diff --git a/application/src/components/schedule-incident/incident-management/OverviewCards.tsx b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx
index 454c21d..73566b9 100644
--- a/application/src/components/schedule-incident/incident-management/OverviewCards.tsx
+++ b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { AlertCircle, CheckCircle, Clock, AlertTriangle, Flag } from 'lucide-react';
import { useLanguage } from '@/contexts/LanguageContext';
import { OverviewCard } from '../common/OverviewCard';
+import { useTheme } from '@/contexts/ThemeContext';
interface OverviewStatsProps {
unresolved: number;
@@ -24,6 +25,7 @@ export const OverviewCards: React.FC
= ({
initialized
}) => {
const { t } = useLanguage();
+ const { theme } = useTheme();
return (
@@ -32,35 +34,55 @@ export const OverviewCards: React.FC
= ({
value={overviewStats.unresolved.toString()}
icon={ }
isLoading={loading && initialized}
- color="red"
+ gradient={
+ theme === "dark"
+ ? "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)"
+ : "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)"
+ }
/>
}
isLoading={loading && initialized}
- color="amber"
+ gradient={
+ theme === "dark"
+ ? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)"
+ : "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)"
+ }
/>
}
isLoading={loading && initialized}
- color="orange"
+ gradient={
+ theme === "dark"
+ ? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)"
+ : "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)"
+ }
/>
}
isLoading={loading && initialized}
- color="green"
+ gradient={
+ theme === "dark"
+ ? "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)"
+ : "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)"
+ }
/>
}
isLoading={loading && initialized}
- color="blue"
+ gradient={
+ theme === "dark"
+ ? "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)"
+ : "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)"
+ }
/>
);
diff --git a/application/src/components/servers/AddServerAgentDialog.tsx b/application/src/components/servers/AddServerAgentDialog.tsx
new file mode 100644
index 0000000..755bb3d
--- /dev/null
+++ b/application/src/components/servers/AddServerAgentDialog.tsx
@@ -0,0 +1,167 @@
+
+import React, { useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+import { Server } from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { getCurrentEndpoint } from "@/lib/pocketbase";
+import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
+import { OneClickInstallTab } from "./OneClickInstallTab";
+import { ManualInstallTab } from "./ManualInstallTab";
+
+interface AddServerAgentDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onAgentAdded: () => void;
+}
+
+export const AddServerAgentDialog: React.FC = ({
+ open,
+ onOpenChange,
+ onAgentAdded,
+}) => {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [activeTab, setActiveTab] = useState("configure");
+
+ // Get current PocketBase URL
+ const currentPocketBaseUrl = getCurrentEndpoint();
+
+ // Form state
+ const [formData, setFormData] = useState({
+ serverName: "",
+ description: "",
+ osType: "",
+ checkInterval: "60",
+ retryAttempt: "3",
+ dockerEnabled: false,
+ notificationEnabled: true,
+ });
+
+ // Generated server token and agent ID
+ const [serverToken] = useState(() =>
+ `srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
+ );
+
+ const [serverId] = useState(() =>
+ `agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
+ );
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (isSubmitting) return;
+
+ if (!formData.serverName || !formData.osType) {
+ toast({
+ title: "Validation Error",
+ description: "Please fill in all required fields.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ // Here you would typically create the server monitoring configuration
+ // For now, we'll simulate the process
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ toast({
+ title: "Server Agent Created",
+ description: `${formData.serverName} monitoring agent has been configured successfully.`,
+ });
+
+ // Switch to one-click install tab after successful creation
+ setActiveTab("one-click");
+ onAgentAdded();
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to create server monitoring agent.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleDialogClose = () => {
+ // Reset form and tab when dialog closes
+ setActiveTab("configure");
+ setFormData({
+ serverName: "",
+ description: "",
+ osType: "",
+ checkInterval: "60",
+ retryAttempt: "3",
+ dockerEnabled: false,
+ notificationEnabled: true,
+ });
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+ Add Server Monitoring Agent
+
+
+ Configure a new server monitoring agent to track system metrics and performance.
+
+
+
+
+
+ Configure Agent
+ One-Click Install
+ Manual Installation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx
new file mode 100644
index 0000000..cce5755
--- /dev/null
+++ b/application/src/components/servers/EditServerDialog.tsx
@@ -0,0 +1,680 @@
+import React, { useState, useEffect } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
+import { useToast } from "@/hooks/use-toast";
+import { pb } from "@/lib/pocketbase";
+import { Server } from "@/types/server.types";
+import { RefreshCw, X } from "lucide-react";
+import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
+import { templateService, NotificationTemplate } from "@/services/templateService";
+import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
+
+interface EditServerDialogProps {
+ server: Server | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onServerUpdated: () => void;
+}
+
+interface ServerFormData {
+ name: string;
+ check_interval: number;
+ retry_attempts: number;
+ docker_monitoring: boolean;
+ notification_enabled: boolean;
+ notification_channels: string[]; // Changed to array for multiple selections
+ threshold_id: string;
+ template_id: string;
+}
+
+interface ThresholdFormData {
+ cpu_threshold: number;
+ ram_threshold: number;
+ disk_threshold: number;
+ network_threshold: number;
+}
+
+export const EditServerDialog: React.FC = ({
+ server,
+ open,
+ onOpenChange,
+ onServerUpdated,
+}) => {
+ const [formData, setFormData] = useState({
+ name: "",
+ check_interval: 60,
+ retry_attempts: 3,
+ docker_monitoring: false,
+ notification_enabled: false,
+ notification_channels: [], // Changed to array
+ threshold_id: "none",
+ template_id: "none",
+ });
+
+ const [thresholdFormData, setThresholdFormData] = useState({
+ cpu_threshold: 80,
+ ram_threshold: 80,
+ disk_threshold: 80,
+ network_threshold: 80,
+ });
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [alertConfigs, setAlertConfigs] = useState([]);
+ const [templates, setTemplates] = useState([]);
+ const [thresholds, setThresholds] = useState([]);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [selectedThreshold, setSelectedThreshold] = useState(null);
+ const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
+ const [loadingTemplates, setLoadingTemplates] = useState(false);
+ const [loadingThresholds, setLoadingThresholds] = useState(false);
+ const { toast } = useToast();
+
+ // Initialize form data when server changes
+ useEffect(() => {
+ if (server) {
+ // console.log("Setting form data for server:", server);
+ // Parse comma-separated notification_id into array
+ const notificationChannels = server.notification_id
+ ? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
+ : [];
+
+ setFormData({
+ name: server.name || "",
+ check_interval: server.check_interval || 60,
+ retry_attempts: 3,
+ docker_monitoring: server.docker === "true",
+ notification_enabled: notificationChannels.length > 0,
+ notification_channels: notificationChannels,
+ threshold_id: server.threshold_id || "none",
+ template_id: server.template_id || "none",
+ });
+ }
+ }, [server]);
+
+ // Load data when dialog opens
+ useEffect(() => {
+ if (open) {
+ loadAlertConfigurations();
+ loadTemplates();
+ loadThresholds();
+ }
+ }, [open]);
+
+ // Load existing threshold data when thresholds are loaded and we have a server with threshold_id
+ useEffect(() => {
+ if (server && server.threshold_id && thresholds.length > 0) {
+ // console.log("Loading existing threshold data for server:", server.threshold_id);
+ const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
+ if (existingThreshold) {
+ // console.log("Found existing threshold:", existingThreshold);
+ setSelectedThreshold(existingThreshold);
+ // Handle the API response format with proper field names and type conversion
+ setThresholdFormData({
+ cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80,
+ ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
+ disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
+ network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
+ });
+ }
+ }
+ }, [server, thresholds]);
+
+ // Update selected template when form data or templates change
+ useEffect(() => {
+ if (formData.template_id && formData.template_id !== "none" && templates.length > 0) {
+ const template = templates.find(t => t.id === formData.template_id);
+ setSelectedTemplate(template || null);
+ } else {
+ setSelectedTemplate(null);
+ }
+ }, [formData.template_id, templates]);
+
+ // Update selected threshold when threshold_id changes in form
+ useEffect(() => {
+ if (formData.threshold_id && formData.threshold_id !== "none" && thresholds.length > 0) {
+ const threshold = thresholds.find(t => t.id === formData.threshold_id);
+ setSelectedThreshold(threshold || null);
+ if (threshold) {
+ // Handle the API response format with proper field names and type conversion
+ setThresholdFormData({
+ cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80,
+ ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80,
+ disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
+ network_threshold: parseInt(String(threshold.network_threshold)) || 80,
+ });
+ }
+ } else if (formData.threshold_id === "none") {
+ setSelectedThreshold(null);
+ setThresholdFormData({
+ cpu_threshold: 80,
+ ram_threshold: 80,
+ disk_threshold: 80,
+ network_threshold: 80,
+ });
+ }
+ }, [formData.threshold_id, thresholds]);
+
+ const loadAlertConfigurations = async () => {
+ try {
+ setLoadingAlertConfigs(true);
+ const configs = await alertConfigService.getAlertConfigurations();
+ setAlertConfigs(configs);
+ } catch (error) {
+ // console.error('Error loading alert configurations:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to load notification channels",
+ });
+ } finally {
+ setLoadingAlertConfigs(false);
+ }
+ };
+
+ const loadTemplates = async () => {
+ try {
+ setLoadingTemplates(true);
+ const templateList = await templateService.getTemplates();
+ setTemplates(templateList);
+ } catch (error) {
+ // console.error('Error loading templates:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to load templates",
+ });
+ } finally {
+ setLoadingTemplates(false);
+ }
+ };
+
+ const loadThresholds = async () => {
+ try {
+ setLoadingThresholds(true);
+ const thresholdList = await serverThresholdService.getServerThresholds();
+ setThresholds(thresholdList);
+ } catch (error) {
+ // console.error('Error loading server thresholds:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to load server thresholds",
+ });
+ } finally {
+ setLoadingThresholds(false);
+ }
+ };
+
+ const handleThresholdUpdate = async () => {
+ if (!selectedThreshold) return;
+
+ try {
+ // Use the correct field name for RAM threshold
+ const updateData = {
+ cpu_threshold: thresholdFormData.cpu_threshold,
+ ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
+ disk_threshold: thresholdFormData.disk_threshold,
+ network_threshold: thresholdFormData.network_threshold,
+ };
+
+ await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
+
+ // Update local state
+ setSelectedThreshold({
+ ...selectedThreshold,
+ ...thresholdFormData,
+ });
+
+ // Update thresholds list
+ setThresholds(prev => prev.map(t =>
+ t.id === selectedThreshold.id
+ ? { ...t, ...thresholdFormData }
+ : t
+ ));
+
+ toast({
+ title: "Threshold updated",
+ description: "Server threshold values have been updated successfully.",
+ });
+ } catch (error) {
+ // console.error('Error updating threshold:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to update threshold values.",
+ });
+ }
+ };
+
+ const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
+ setFormData(prev => ({
+ ...prev,
+ notification_channels: checked
+ ? [...prev.notification_channels, channelId]
+ : prev.notification_channels.filter(id => id !== channelId)
+ }));
+ };
+
+ const removeNotificationChannel = (channelId: string) => {
+ setFormData(prev => ({
+ ...prev,
+ notification_channels: prev.notification_channels.filter(id => id !== channelId)
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!server || isSubmitting) return;
+
+ try {
+ setIsSubmitting(true);
+
+ // Convert notification channels array to comma-separated string
+ const notificationChannelsString = formData.notification_enabled
+ ? formData.notification_channels.join(',')
+ : "";
+
+ const updateData = {
+ name: formData.name,
+ check_interval: formData.check_interval,
+ docker: formData.docker_monitoring ? "true" : "false",
+ notification_id: notificationChannelsString,
+ threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "",
+ template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "",
+ updated: new Date().toISOString(),
+ };
+
+ await pb.collection('servers').update(server.id, updateData);
+
+ toast({
+ title: "Server updated",
+ description: `${formData.name} has been updated successfully.`,
+ });
+
+ onServerUpdated();
+ onOpenChange(false);
+
+ } catch (error) {
+ // console.error('Error updating server:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to update server. Please try again.",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (server) {
+ const notificationChannels = server.notification_id
+ ? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
+ : [];
+
+ setFormData({
+ name: server.name || "",
+ check_interval: server.check_interval || 60,
+ retry_attempts: 3,
+ docker_monitoring: server.docker === "true",
+ notification_enabled: notificationChannels.length > 0,
+ notification_channels: notificationChannels,
+ threshold_id: server.threshold_id || "none",
+ template_id: server.template_id || "none",
+ });
+ }
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+ Edit Server Configuration
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/ManualInstallTab.tsx b/application/src/components/servers/ManualInstallTab.tsx
new file mode 100644
index 0000000..9d289a8
--- /dev/null
+++ b/application/src/components/servers/ManualInstallTab.tsx
@@ -0,0 +1,125 @@
+
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Copy, Terminal } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { copyToClipboard } from "@/utils/copyUtils";
+
+interface ManualInstallTabProps {
+ serverToken: string;
+ currentPocketBaseUrl: string;
+ formData: {
+ serverName: string;
+ osType: string;
+ checkInterval: string;
+ };
+ serverId: string;
+ onDialogClose: () => void;
+}
+
+export const ManualInstallTab: React.FC = ({
+ serverToken,
+ currentPocketBaseUrl,
+ formData,
+ serverId,
+ onDialogClose,
+}) => {
+ const getManualInstallSteps = () => {
+ const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
+
+ return [
+ {
+ title: "Download the installation script",
+ command: `curl -L -o server-agent.sh "${scriptUrl}"`
+ },
+ {
+ title: "Make the script executable",
+ command: `chmod +x server-agent.sh`
+ },
+ {
+ title: "Run the installation with your configuration",
+ command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh`
+ }
+ ];
+ };
+
+ return (
+
+
+
+
+ Manual Installation Steps
+
+
+ Step-by-step installation process
+
+
+
+
+
+
+ Server Name: {formData.serverName}
+
+
+ Agent ID: {serverId}
+
+
+ OS Type: {formData.osType}
+
+
+ Check Interval: {formData.checkInterval}s
+
+
+
+
+
+ {getManualInstallSteps().map((step, index) => (
+
+
+
+ {index + 1}
+
+ {step.title}
+
+
+
+ {step.command}
+
+
copyToClipboard(step.command)}
+ >
+
+ Copy
+
+
+
+ ))}
+
+
+
+
Prerequisites:
+
+ Ensure you have root/sudo access on the target server
+ Make sure curl is installed for downloading files
+ Internet connection required for downloading script
+
+
+
After Installation:
+
+ The agent will start automatically and appear in your dashboard within a few minutes.
+
+
+
+
+
+ Done
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/OSSelector.tsx b/application/src/components/servers/OSSelector.tsx
new file mode 100644
index 0000000..6147997
--- /dev/null
+++ b/application/src/components/servers/OSSelector.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { Check } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface OSOption {
+ value: string;
+ name: string;
+ logo: string; // now it's a path to image, e.g., /logos/ubuntu.svg
+}
+
+const osOptions: OSOption[] = [
+ { value: "ubuntu", name: "Ubuntu", logo: "/upload/os/ubuntu.png" },
+ { value: "debian", name: "Debian", logo: "/upload/os/debian.png" },
+ { value: "centos", name: "CentOS", logo: "/upload/os/centos.png" },
+ { value: "rhel", name: "Red Hat Enterprise Linux", logo: "/upload/os/rhel.png" },
+ { value: "linux", name: "Linux (Generic)", logo: "/upload/os/linux.png" },
+ { value: "windows", name: "Windows Server", logo: "/upload/os/windows.png" },
+];
+
+interface OSSelectorProps {
+ value: string;
+ onValueChange: (value: string) => void;
+}
+
+export const OSSelector: React.FC = ({ value, onValueChange }) => {
+ return (
+
+ {osOptions.map((os) => (
+
onValueChange(os.value)}
+ className={cn(
+ "flex items-center justify-between w-full rounded-md border px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition",
+ value === os.value ? "bg-accent text-accent-foreground border-ring" : "border-input"
+ )}
+ >
+
+
+
{os.name}
+
+ {value === os.value && }
+
+ ))}
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx
new file mode 100644
index 0000000..1067d20
--- /dev/null
+++ b/application/src/components/servers/OneClickInstallTab.tsx
@@ -0,0 +1,101 @@
+
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Copy, Download } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { copyToClipboard } from "@/utils/copyUtils";
+
+interface OneClickInstallTabProps {
+ serverToken: string;
+ currentPocketBaseUrl: string;
+ formData: {
+ serverName: string;
+ osType: string;
+ checkInterval: string;
+ retryAttempt: string;
+ };
+ serverId: string;
+ onDialogClose: () => void;
+}
+
+export const OneClickInstallTab: React.FC = ({
+ serverToken,
+ currentPocketBaseUrl,
+ formData,
+ serverId,
+ onDialogClose,
+}) => {
+ const getOneClickInstallCommand = () => {
+ const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
+
+ return `curl -L -o server-agent.sh "${scriptUrl}"
+chmod +x server-agent.sh
+SERVER_TOKEN="${serverToken}" \\
+POCKETBASE_URL="${currentPocketBaseUrl}" \\
+SERVER_NAME="${formData.serverName}" \\
+AGENT_ID="${serverId}" \\
+OS_TYPE="${formData.osType}" \\
+CHECK_INTERVAL="${formData.checkInterval}" \\
+RETRY_ATTEMPTS="${formData.retryAttempt}" \\
+sudo -E bash ./server-agent.sh`;
+ };
+
+ const handleCopyCommand = async (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('Copy button clicked'); // Debug log
+ const command = getOneClickInstallCommand();
+ console.log('Copying command:', command); // Debug log
+ await copyToClipboard(command);
+ };
+
+ return (
+
+
+
+
+ One-Click Install
+
+
+ Copy and paste this single command to install the monitoring agent instantly
+
+
+
+
+
Quick Install Command
+
+
+ {getOneClickInstallCommand()}
+
+
+
+ Copy
+
+
+
+
+
+
Simply run this command on your server:
+
+ SSH into your target server
+ Paste and run the command above
+ The agent will be installed and started automatically
+
+
+
+
+
+ Done
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/ServerAgentConfigForm.tsx b/application/src/components/servers/ServerAgentConfigForm.tsx
new file mode 100644
index 0000000..da2e858
--- /dev/null
+++ b/application/src/components/servers/ServerAgentConfigForm.tsx
@@ -0,0 +1,172 @@
+
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Copy } from "lucide-react";
+import { copyToClipboard } from "@/utils/copyUtils";
+import { OSSelector } from "./OSSelector";
+
+interface ServerAgentConfigFormProps {
+ formData: {
+ serverName: string;
+ description: string;
+ osType: string;
+ checkInterval: string;
+ retryAttempt: string;
+ dockerEnabled: boolean;
+ notificationEnabled: boolean;
+ };
+ setFormData: React.Dispatch>;
+ serverId: string;
+ serverToken: string;
+ currentPocketBaseUrl: string;
+ isSubmitting: boolean;
+ onSubmit: (e: React.FormEvent) => void;
+}
+
+export const ServerAgentConfigForm: React.FC = ({
+ formData,
+ setFormData,
+ serverId,
+ serverToken,
+ currentPocketBaseUrl,
+ isSubmitting,
+ onSubmit,
+}) => {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx
index 274f966..63e313d 100644
--- a/application/src/components/servers/ServerHistoryCharts.tsx
+++ b/application/src/components/servers/ServerHistoryCharts.tsx
@@ -24,7 +24,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
isFetching
} = useServerHistoryData(serverId);
- //console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange);
+ // console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange);
// Memoize latest data calculation to prevent unnecessary recalculations
const latestData = useMemo(() => {
@@ -48,10 +48,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
{/* Skeleton loading cards */}
-
+
{[1, 2, 3, 4].map((index) => (
-
+
@@ -60,7 +60,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
-
+
@@ -122,11 +122,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
);
}
- // console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange);
+// console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange);
return (
-
+
Historical Performance
@@ -143,12 +143,20 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
- {/* Use CSS Grid for better performance than flexbox */}
-
-
-
-
-
+ {/* Improved responsive grid layout */}
+
);
diff --git a/application/src/components/servers/ServerMetricsCharts.tsx b/application/src/components/servers/ServerMetricsCharts.tsx
index a3fa10c..1a60497 100644
--- a/application/src/components/servers/ServerMetricsCharts.tsx
+++ b/application/src/components/servers/ServerMetricsCharts.tsx
@@ -28,7 +28,10 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
queryKey: ['server-metrics', serverId, timeRange],
queryFn: () => serverService.getServerMetrics(serverId, timeRange),
enabled: !!serverId,
- refetchInterval: 30000
+ refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Increased intervals
+ staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
+ gcTime: 10 * 60 * 1000, // 10 minutes cache
+ refetchOnWindowFocus: false, // Prevent refetch on window focus
});
const chartData = formatChartData(metrics, timeRange);
diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx
index c73172f..afea63c 100644
--- a/application/src/components/servers/ServerTable.tsx
+++ b/application/src/components/servers/ServerTable.tsx
@@ -1,4 +1,3 @@
-
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -6,13 +5,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
-import { Progress } from "@/components/ui/progress";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
import { Server } from "@/types/server.types";
import { ServerStatusBadge } from "./ServerStatusBadge";
import { OSTypeIcon } from "./OSTypeIcon";
+import { EditServerDialog } from "./EditServerDialog";
import { serverService } from "@/services/serverService";
+import { useToast } from "@/hooks/use-toast";
+import { pb } from "@/lib/pocketbase";
+import { useTheme } from "@/contexts/ThemeContext";
interface ServerTableProps {
servers: Server[];
@@ -21,9 +24,15 @@ interface ServerTableProps {
}
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
+ const { theme } = useTheme();
const [searchTerm, setSearchTerm] = useState("");
- const [pausedServers, setPausedServers] = useState
>(new Set());
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [editDialogOpen, setEditDialogOpen] = useState(false);
+ const [selectedServer, setSelectedServer] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [pausingServers, setPausingServers] = useState>(new Set());
const navigate = useNavigate();
+ const { toast } = useToast();
const filteredServers = servers.filter(server =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -39,38 +48,165 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
navigate(`/container-monitoring/${serverId}`);
};
- const handlePauseResume = (serverId: string) => {
- const isPaused = pausedServers.has(serverId);
- if (isPaused) {
- setPausedServers(prev => {
+ const handlePauseResume = async (server: Server) => {
+ const serverId = server.id;
+ const isPaused = server.status === "paused";
+
+ if (pausingServers.has(serverId)) {
+ return; // Already processing this server
+ }
+
+ try {
+ setPausingServers(prev => new Set(prev).add(serverId));
+
+ // Only update the status field, preserving all other server configuration
+ const updateData = {
+ status: isPaused ? "up" : "paused",
+ last_checked: new Date().toISOString()
+ };
+
+ await pb.collection('servers').update(serverId, updateData);
+
+ toast({
+ title: isPaused ? "Server resumed" : "Server paused",
+ description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
+ });
+
+ // console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
+
+ // Refresh the server list to show updated status
+ onRefresh();
+
+ } catch (error) {
+ // console.error('Error updating server status:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
+ });
+ } finally {
+ setPausingServers(prev => {
const newSet = new Set(prev);
newSet.delete(serverId);
return newSet;
});
- // console.log('Resume server monitoring:', serverId);
- } else {
- setPausedServers(prev => new Set(prev).add(serverId));
- // console.log('Pause server monitoring:', serverId);
}
};
- const handleEdit = (serverId: string) => {
- // TODO: Implement edit functionality
- // console.log('Edit server:', serverId);
+ const handleEdit = (server: Server) => {
+ setSelectedServer(server);
+ setEditDialogOpen(true);
};
- const handleDelete = (serverId: string) => {
- // TODO: Implement delete functionality
- // console.log('Delete server:', serverId);
+ const handleDelete = (server: Server) => {
+ setSelectedServer(server);
+ setDeleteDialogOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!selectedServer || isDeleting) return;
+
+ try {
+ setIsDeleting(true);
+
+ // Delete the server from the database
+ await pb.collection('servers').delete(selectedServer.id);
+
+ toast({
+ title: "Server deleted",
+ description: `${selectedServer.name} has been deleted successfully.`,
+ });
+
+ // Refresh the server list
+ onRefresh();
+
+ // Close the dialog
+ setDeleteDialogOpen(false);
+ setSelectedServer(null);
+
+ } catch (error) {
+ // console.error('Error deleting server:', error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to delete server. Please try again.",
+ });
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ const CustomProgressBar = ({
+ value,
+ label,
+ subtitle,
+ type
+ }: {
+ value: number;
+ label: string;
+ subtitle: string;
+ type: 'cpu' | 'memory' | 'disk'
+ }) => {
+ const getGradientColors = (type: string, value: number) => {
+ if (type === 'cpu') {
+ if (value > 90) return 'from-red-500 to-red-600';
+ if (value > 75) return 'from-orange-500 to-orange-600';
+ if (value > 60) return 'from-yellow-500 to-yellow-600';
+ return 'from-green-500 to-green-600';
+ }
+ if (type === 'memory') {
+ if (value > 90) return 'from-red-500 to-red-600';
+ if (value > 75) return 'from-yellow-500 to-yellow-600';
+ return 'from-blue-500 to-blue-600';
+ }
+ if (type === 'disk') {
+ if (value > 95) return 'from-red-500 to-red-600';
+ if (value > 85) return 'from-yellow-500 to-yellow-600';
+ return 'from-orange-500 to-orange-600';
+ }
+ return 'from-gray-500 to-gray-600';
+ };
+
+ const getTextColor = (value: number) => {
+ if (value > 90) return 'text-red-600 dark:text-red-400';
+ if (value > 75) return 'text-orange-600 dark:text-orange-400';
+ if (value > 60) return 'text-yellow-600 dark:text-yellow-400';
+ return 'text-green-600 dark:text-green-400';
+ };
+
+ return (
+
+
+
+ {label}
+
+
+ {subtitle}
+
+
+
+
+ );
};
if (isLoading) {
return (
-
-
+
+
Servers
-
+
Loading servers...
@@ -81,189 +217,218 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
}
return (
-
-
-
-
Servers
-
-
-
-
setSearchTerm(e.target.value)}
- className="pl-8"
- />
+ <>
+
+
+
+
Servers
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-8"
+ />
+
+
+
+
-
-
-
-
-
-
- {filteredServers.length === 0 ? (
-
- ) : (
-
-
-
-
- Name
- Status
- OS
- IP Address
- CPU
- Memory
- Disk
- Uptime
- Last Checked
- Actions
-
-
-
- {filteredServers.map((server) => {
- const cpuUsage = server.cpu_usage || 0;
- const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
- const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
- const isPaused = pausedServers.has(server.id);
+
+
+ {filteredServers.length === 0 ? (
+
+ ) : (
+
+
+
+
+ Name
+ Status
+ OS
+ IP Address
+ CPU
+ Memory
+ Disk
+ Uptime
+ Last Checked
+ Actions
+
+
+
+ {filteredServers.map((server) => {
+ const cpuUsage = server.cpu_usage || 0;
+ const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
+ const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
+ const isPaused = server.status === "paused";
+ const isProcessing = pausingServers.has(server.id);
- return (
-
-
-
-
-
-
-
-
-
-
- {server.os_type}
-
-
-
- {server.ip_address}
-
-
-
-
-
{cpuUsage.toFixed(1)}%
-
{server.cpu_cores} cores
+ return (
+
handleViewDetails(server.id)}
+ >
+
+
+ {server.name}
- 90 ? "bg-red-00" :
- cpuUsage > 75 ? "bg-orange-500" :
- cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500"
- }
- />
-
-
-
-
-
-
{memoryUsage.toFixed(1)}%
-
{serverService.formatBytes(server.ram_total)}
+
+
+
+
+
+
+
+
+ {server.os_type}
+
- 90 ? "bg-red-500" :
- memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500"
- }
+
+
+
+ {server.ip_address}
+
+
+
+
-
-
-
-
-
-
{diskUsage.toFixed(1)}%
-
{serverService.formatBytes(server.disk_total)}
+
+
+
+
+
+
+
+
+
+ {server.uptime}
- 95 ? "bg-red-500" :
- diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500"
- }
- />
-
-
-
- {server.uptime}
-
-
-
-
- {new Date(server.last_checked).toLocaleString()}
-
-
-
-
-
-
- Open menu
-
-
-
-
- handleViewDetails(server.id)}>
-
- View Server Detail
-
- {server.docker === 'true' && (
- handleViewContainers(server.id)}>
-
- Container Monitoring
+
+
+
+ {new Date(server.last_checked).toLocaleString()}
+
+
+
e.stopPropagation()}>
+
+
+
+ Open menu
+ {isProcessing ? (
+
+ ) : (
+
+ )}
+
+
+
+ { e.stopPropagation(); handleViewDetails(server.id); }}>
+
+ View Server Detail
- )}
-
- handlePauseResume(server.id)}>
- {isPaused ? (
- <>
-
- Resume Monitoring
- >
- ) : (
- <>
-
- Pause Monitoring
- >
+ {server.docker === 'true' && (
+ { e.stopPropagation(); handleViewContainers(server.id); }}>
+
+ Container Monitoring
+
)}
-
-
- handleEdit(server.id)}>
-
- Edit Server
-
- handleDelete(server.id)}
- className="text-red-600 focus:text-red-600"
- >
-
- Delete Server
-
-
-
-
-
- );
- })}
-
-
-
- )}
-
-
+
+ { e.stopPropagation(); handlePauseResume(server); }}
+ disabled={isProcessing}
+ >
+ {isPaused ? (
+ <>
+
+ Resume Monitoring
+ >
+ ) : (
+ <>
+
+ Pause Monitoring
+ >
+ )}
+
+
+ { e.stopPropagation(); handleEdit(server); }}>
+
+ Edit Server
+
+ { e.stopPropagation(); handleDelete(server); }}
+ className="text-red-600 focus:text-red-600"
+ >
+
+ Delete Server
+
+
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* Edit Server Dialog */}
+
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Are you sure you want to delete this server?
+
+ This action cannot be undone. This will permanently delete{' '}
+
+ {selectedServer?.name}
+ {' '}
+ and all of its monitoring data.
+
+
+
+
+ Cancel
+
+
+ {isDeleting ? "Deleting..." : "Delete"}
+
+
+
+
+ >
);
-};
\ No newline at end of file
+};
diff --git a/application/src/components/servers/charts/CPUChart.tsx b/application/src/components/servers/charts/CPUChart.tsx
index c5fb013..29dff23 100644
--- a/application/src/components/servers/charts/CPUChart.tsx
+++ b/application/src/components/servers/charts/CPUChart.tsx
@@ -1,7 +1,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
-import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
import { Cpu } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
@@ -18,65 +18,62 @@ export const CPUChart = ({ data, latestData }: CPUChartProps) => {
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
return (
-
-
+
+
-
+
- CPU Usage
+
CPU Usage
{latestData && (
-
+
{latestData.cpuUsage}%
{latestData.cpuCores} cores
)}
-
-
-
-
-
-
-
-
-
-
-
-
- }
- cursor={{ stroke: getGridColor() }}
- />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ cursor={{ stroke: getGridColor() }}
+ />
+
+
+
+
);
diff --git a/application/src/components/servers/charts/DiskChart.tsx b/application/src/components/servers/charts/DiskChart.tsx
index da82c15..d94098c 100644
--- a/application/src/components/servers/charts/DiskChart.tsx
+++ b/application/src/components/servers/charts/DiskChart.tsx
@@ -1,7 +1,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
-import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
import { HardDrive } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
@@ -18,65 +18,62 @@ export const DiskChart = ({ data, latestData }: DiskChartProps) => {
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
return (
-
-
+
+
-
+
- Disk Usage
+
Disk Usage
{latestData && (
-
+
{latestData.diskUsagePercent}%
{latestData.diskUsed} / {latestData.diskTotal}
)}
-
-
-
-
-
-
-
-
-
-
-
-
- }
- cursor={{ stroke: getGridColor() }}
- />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ cursor={{ stroke: getGridColor() }}
+ />
+
+
+
+
);
diff --git a/application/src/components/servers/charts/MemoryChart.tsx b/application/src/components/servers/charts/MemoryChart.tsx
index a0abd7b..be341ff 100644
--- a/application/src/components/servers/charts/MemoryChart.tsx
+++ b/application/src/components/servers/charts/MemoryChart.tsx
@@ -1,7 +1,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
-import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
import { MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
@@ -18,65 +18,62 @@ export const MemoryChart = ({ data, latestData }: MemoryChartProps) => {
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
return (
-
-
+
+
-
+
- Memory Usage
+
Memory Usage
{latestData && (
-
+
{latestData.ramUsagePercent}%
{latestData.ramUsed} / {latestData.ramTotal}
)}
-
-
-
-
-
-
-
-
-
-
-
-
- }
- cursor={{ stroke: getGridColor() }}
- />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ cursor={{ stroke: getGridColor() }}
+ />
+
+
+
+
);
diff --git a/application/src/components/servers/charts/NetworkChart.tsx b/application/src/components/servers/charts/NetworkChart.tsx
index cbb25b5..69985d6 100644
--- a/application/src/components/servers/charts/NetworkChart.tsx
+++ b/application/src/components/servers/charts/NetworkChart.tsx
@@ -1,7 +1,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
-import { LineChart, Line, XAxis, YAxis, CartesianGrid } from "recharts";
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
import { Wifi } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { NetworkTooltipContent } from "./tooltips/NetworkTooltipContent";
@@ -18,84 +18,77 @@ export const NetworkChart = ({ data, latestData }: NetworkChartProps) => {
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
return (
-
-
+
+
-
+
- Network Traffic
+
Network Traffic
{latestData && (
-
+
{latestData.networkRxSpeed} KB/s β
{latestData.networkTxSpeed} KB/s β
)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- cursor={{ stroke: getGridColor() }}
- />
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ cursor={{ stroke: getGridColor() }}
+ />
+
+
+
+
+
);
diff --git a/application/src/components/servers/charts/dataUtils.ts b/application/src/components/servers/charts/dataUtils.ts
index d58e2a0..4f3355e 100644
--- a/application/src/components/servers/charts/dataUtils.ts
+++ b/application/src/components/servers/charts/dataUtils.ts
@@ -1,4 +1,3 @@
-
export const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 B';
const k = 1024;
@@ -49,34 +48,82 @@ export const timeRangeOptions = [
{ value: '3m' as TimeRange, label: 'Last 90 days', hours: 24 * 90 },
];
-// Optimized time range filtering with early returns
export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): any[] => {
- if (!metrics?.length) return [];
+ if (!metrics?.length) {
+ // console.log('π filterMetricsByTimeRange: No metrics provided');
+ return [];
+ }
+
+ //console.log('π filterMetricsByTimeRange: Starting with', metrics.length, 'metrics for', timeRange);
const now = new Date();
+
+ // For 60m, let's be very specific about what we're looking for
+ if (timeRange === '60m') {
+ // console.log('β° 60m filter: Current time:', now.toISOString());
+
+ // Try exact 60 minutes first
+ const cutoffTime60m = new Date(now.getTime() - (60 * 60 * 1000));
+ // console.log('β° 60m filter: Cutoff time:', cutoffTime60m.toISOString());
+
+ const filtered60m = metrics.filter(metric => {
+ const metricTime = new Date(metric.created || metric.timestamp);
+ const isWithinRange = metricTime >= cutoffTime60m && metricTime <= now;
+
+ if (!isWithinRange) {
+ const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60));
+ // console.log('β° Excluding record:', {
+ // created: metric.created || metric.timestamp,
+ // ageMinutes: ageMinutes,
+ // reason: ageMinutes > 60 ? 'too old' : 'future date'
+ // });
+ }
+
+ return isWithinRange;
+ });
+
+ // console.log('β
60m strict filter result:', filtered60m.length, 'records');
+
+ if (filtered60m.length > 0) {
+ return filtered60m.sort((a, b) =>
+ new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime()
+ );
+ }
+
+ // If no data in exactly 60m, show what we have and return it anyway for debugging
+ // console.log('β οΈ No data in 60m range, showing all available data ages:');
+ metrics.forEach((metric, index) => {
+ const metricTime = new Date(metric.created || metric.timestamp);
+ const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60));
+ // console.log(`Record ${index}: ${metric.created || metric.timestamp} (${ageMinutes} minutes ago)`);
+ });
+
+ // Return the most recent data regardless of age
+ const sorted = metrics.sort((a, b) =>
+ new Date(b.created || b.timestamp).getTime() - new Date(a.created || a.timestamp).getTime()
+ );
+ // console.log('π Returning most recent available data for 60m view');
+ return sorted.slice(0, 20); // Show last 20 records
+ }
+
+ // For other time ranges, use normal filtering
const selectedRange = timeRangeOptions.find(opt => opt.value === timeRange);
if (!selectedRange) return metrics;
- // Add small buffer to avoid edge cases
- const bufferMinutes = timeRange === '60m' ? 5 : timeRange === '1d' ? 30 : 60;
- const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000) - (bufferMinutes * 60 * 1000));
-
-// console.log('filterMetricsByTimeRange: timeRange:', timeRange, 'cutoffTime:', cutoffTime.toISOString());
+ const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000));
const filtered = metrics.filter(metric => {
const metricTime = new Date(metric.created || metric.timestamp);
return metricTime >= cutoffTime && metricTime <= now;
});
-// console.log('filterMetricsByTimeRange: Filtered', metrics.length, 'to', filtered.length, 'metrics');
+ // console.log('β
Filtered', metrics.length, 'to', filtered.length, 'metrics for', timeRange);
- // Sort by timestamp for proper chart display
return filtered.sort((a, b) =>
new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime()
);
};
-// Optimized timestamp formatting with caching
const timestampCache = new Map();
const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
@@ -118,7 +165,6 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
});
}
- // Cache the result (limit cache size)
if (timestampCache.size > 1000) {
timestampCache.clear();
}
@@ -127,37 +173,49 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
return formatted;
};
-// Optimized chart data formatting with better performance
export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
-// console.log('formatChartData: Input metrics count:', metrics?.length || 0, 'timeRange:', timeRange);
+ // console.log('π formatChartData: Processing', {
+ // inputCount: metrics?.length || 0,
+ // timeRange,
+ // sampleMetric: metrics?.[0] ? {
+ // id: metrics[0].id,
+ // created: metrics[0].created,
+ // timestamp: metrics[0].timestamp,
+ // server_id: metrics[0].server_id
+ // } : null
+ // });
- if (!metrics?.length) return [];
+ if (!metrics?.length) {
+ // console.log('β formatChartData: No metrics provided');
+ return [];
+ }
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
-// console.log('formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics');
+ // console.log('π formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics');
- if (!filteredMetrics.length) return [];
+ if (!filteredMetrics.length) {
+ // console.log('β formatChartData: No metrics after time filtering');
+ return [];
+ }
// Dynamic sampling based on time range and data volume
let maxDataPoints: number;
switch (timeRange) {
- case '60m': maxDataPoints = 60; break; // 1 point per minute max
- case '1d': maxDataPoints = 144; break; // 1 point per 10 minutes max
- case '7d': maxDataPoints = 168; break; // 1 point per hour max
- case '1m': maxDataPoints = 120; break; // 1 point per 6 hours max
- case '3m': maxDataPoints = 90; break; // 1 point per day max
+ case '60m': maxDataPoints = 60; break;
+ case '1d': maxDataPoints = 144; break;
+ case '7d': maxDataPoints = 168; break;
+ case '1m': maxDataPoints = 120; break;
+ case '3m': maxDataPoints = 90; break;
default: maxDataPoints = 100;
}
- // Smart sampling - only sample if we have significantly more data
const sampledMetrics = filteredMetrics.length > maxDataPoints * 1.2
? filteredMetrics.filter((_, index) => index % Math.ceil(filteredMetrics.length / maxDataPoints) === 0)
: filteredMetrics;
-// console.log('formatChartData: After sampling:', sampledMetrics.length, 'metrics for display');
+ // console.log('π formatChartData: After sampling:', sampledMetrics.length, 'metrics for display');
- // Batch process the data transformation for better performance
- return sampledMetrics.map((metric) => {
+ const formattedData = sampledMetrics.map((metric) => {
const cpuUsage = typeof metric.cpu_usage === 'string' ?
parseFloat(metric.cpu_usage.replace('%', '')) :
parseFloat(metric.cpu_usage) || 0;
@@ -210,4 +268,7 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
networkTxSpeed: Math.round(networkTxSpeed * 100) / 100,
};
});
+
+ // console.log('β
formatChartData: Final formatted data count:', formattedData.length);
+ return formattedData;
};
\ No newline at end of file
diff --git a/application/src/components/servers/charts/hooks/useServerHistoryData.ts b/application/src/components/servers/charts/hooks/useServerHistoryData.ts
index d08a437..04e0365 100644
--- a/application/src/components/servers/charts/hooks/useServerHistoryData.ts
+++ b/application/src/components/servers/charts/hooks/useServerHistoryData.ts
@@ -1,5 +1,5 @@
-import { useState } from "react";
+import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { serverService } from "@/services/serverService";
import { formatChartData } from "../dataUtils";
@@ -7,7 +7,7 @@ import { formatChartData } from "../dataUtils";
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
export const useServerHistoryData = (serverId: string) => {
- const [timeRange, setTimeRange] = useState("1d");
+ const [timeRange, setTimeRange] = useState("60m");
const {
data: metrics = [],
@@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => {
return result || [];
},
enabled: !!serverId,
- refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency
- staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time
- retry: 2, // Reduced retries
- retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry
- gcTime: 5 * 60 * 1000, // 5 minutes cache
+ refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals
+ staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
+ retry: 1, // Reduced retries to prevent excessive requests
+ retryDelay: 2000, // Slower retry
+ gcTime: 10 * 60 * 1000, // 10 minutes cache
+ refetchOnWindowFocus: false, // Prevent refetch on window focus
+ refetchOnMount: false, // Prevent refetch on mount if data exists
});
// Memoize chart data formatting to prevent unnecessary recalculations
- const chartData = formatChartData(metrics, timeRange);
+ const chartData = useMemo(() => {
+ return formatChartData(metrics, timeRange);
+ }, [metrics, timeRange]);
return {
timeRange,
diff --git a/application/src/components/services/AddServiceDialog.tsx b/application/src/components/services/AddServiceDialog.tsx
index 4d37c26..13e5947 100644
--- a/application/src/components/services/AddServiceDialog.tsx
+++ b/application/src/components/services/AddServiceDialog.tsx
@@ -8,6 +8,7 @@ import {
} from "@/components/ui/dialog";
import { ServiceForm } from "./ServiceForm";
import { ScrollArea } from "@/components/ui/scroll-area";
+import { useQueryClient } from "@tanstack/react-query";
interface AddServiceDialogProps {
open: boolean;
@@ -15,7 +16,11 @@ interface AddServiceDialogProps {
}
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
- const handleSuccess = () => {
+ const queryClient = useQueryClient();
+ const handleSuccess = async () => {
+ // Immediately invalidate and refetch services data
+ await queryClient.invalidateQueries({ queryKey: ["services"] });
+ await queryClient.refetchQueries({ queryKey: ["services"] });
onOpenChange(false);
};
diff --git a/application/src/components/services/HeatmapChart.tsx b/application/src/components/services/HeatmapChart.tsx
new file mode 100644
index 0000000..f0b5539
--- /dev/null
+++ b/application/src/components/services/HeatmapChart.tsx
@@ -0,0 +1,174 @@
+
+import { UptimeData } from "@/types/service.types";
+import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay } from "date-fns";
+
+interface HeatmapChartProps {
+ uptimeData: UptimeData[];
+ selectedMonth: Date;
+}
+
+export const HeatmapChart = ({ uptimeData, selectedMonth }: HeatmapChartProps) => {
+ const monthStart = startOfMonth(selectedMonth);
+ const monthEnd = endOfMonth(selectedMonth);
+ const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd });
+
+ const getStatusForDay = (day: Date) => {
+ const dayData = uptimeData.filter(data =>
+ isSameDay(new Date(data.timestamp), day)
+ );
+
+ if (dayData.length === 0) return 'no-data';
+
+ // Calculate the predominant status for the day
+ const statusCounts = dayData.reduce((acc, data) => {
+ acc[data.status] = (acc[data.status] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ const predominantStatus = Object.entries(statusCounts)
+ .sort(([,a], [,b]) => b - a)[0][0];
+
+ return predominantStatus;
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'up':
+ return 'bg-emerald-500';
+ case 'down':
+ return 'bg-red-500';
+ case 'warning':
+ return 'bg-amber-500';
+ case 'paused':
+ return 'bg-slate-500';
+ default:
+ return 'bg-slate-700';
+ }
+ };
+
+ const getStatusLabel = (status: string) => {
+ switch (status) {
+ case 'up':
+ return 'Up';
+ case 'down':
+ return 'Down';
+ case 'warning':
+ return 'Warning';
+ case 'paused':
+ return 'Paused';
+ default:
+ return 'No Data';
+ }
+ };
+
+ // Group days by weeks
+ const weeks: Date[][] = [];
+ let currentWeek: Date[] = [];
+
+ daysInMonth.forEach((day, index) => {
+ if (index === 0) {
+ // Fill empty days at the start of the month
+ const dayOfWeek = day.getDay();
+ for (let i = 0; i < dayOfWeek; i++) {
+ currentWeek.push(new Date(0)); // Placeholder for empty cells
+ }
+ }
+
+ currentWeek.push(day);
+
+ if (currentWeek.length === 7) {
+ weeks.push([...currentWeek]);
+ currentWeek = [];
+ }
+ });
+
+ // Add remaining days to last week
+ if (currentWeek.length > 0) {
+ while (currentWeek.length < 7) {
+ currentWeek.push(new Date(0)); // Placeholder for empty cells
+ }
+ weeks.push(currentWeek);
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Service Health - {format(selectedMonth, 'MMMM yyyy')}
+
+
+ Daily status overview for the current month
+
+
+
+ {/* Calendar Grid */}
+
+ {/* Day labels */}
+
+
Sun
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
+
+ {/* Calendar days */}
+
+ {weeks.map((week, weekIndex) => (
+
+ {week.map((day, dayIndex) => {
+ const isPlaceholder = day.getTime() === 0;
+ const status = isPlaceholder ? 'no-data' : getStatusForDay(day);
+ const dayData = isPlaceholder ? [] : uptimeData.filter(data =>
+ isSameDay(new Date(data.timestamp), day)
+ );
+
+ return (
+
0 ? ` (${dayData.length} checks)` : ''}`}
+ >
+ {isPlaceholder ? '' : format(day, 'd')}
+
+ );
+ })}
+
+ ))}
+
+
+
+ {/* Status Legend */}
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/services/HeatmapDialog.tsx b/application/src/components/services/HeatmapDialog.tsx
new file mode 100644
index 0000000..ef15c8b
--- /dev/null
+++ b/application/src/components/services/HeatmapDialog.tsx
@@ -0,0 +1,97 @@
+
+import { useState } from "react";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { HeatmapChart } from "./HeatmapChart";
+import { UptimeData } from "@/types/service.types";
+import { addMonths, subMonths, format } from "date-fns";
+
+interface HeatmapDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ serviceName: string;
+ uptimeData: UptimeData[];
+}
+
+export const HeatmapDialog = ({
+ open,
+ onOpenChange,
+ serviceName,
+ uptimeData
+}: HeatmapDialogProps) => {
+ const [selectedMonth, setSelectedMonth] = useState(new Date());
+
+ const handlePreviousMonth = () => {
+ setSelectedMonth(prev => subMonths(prev, 1));
+ };
+
+ const handleNextMonth = () => {
+ setSelectedMonth(prev => addMonths(prev, 1));
+ };
+
+ const handleCurrentMonth = () => {
+ setSelectedMonth(new Date());
+ };
+
+ return (
+
+
+
+
+ Health Heatmap - {serviceName}
+
+
+ Monthly overview of service health status with daily breakdown
+
+
+
+
+ {/* Month Navigation */}
+
+
+
+ Previous
+
+
+
+ Current Month
+
+
+
+ Next
+
+
+
+
+ {/* Heatmap Chart */}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/services/ResponseTimeChart.tsx b/application/src/components/services/ResponseTimeChart.tsx
index 51d04e2..3d70d59 100644
--- a/application/src/components/services/ResponseTimeChart.tsx
+++ b/application/src/components/services/ResponseTimeChart.tsx
@@ -1,27 +1,34 @@
-import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts";
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line, BarChart, Bar } from "recharts";
import { UptimeData } from "@/types/service.types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useTheme } from "@/contexts/ThemeContext";
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { format } from "date-fns";
+import { Button } from "@/components/ui/button";
+import { AreaChart as AreaChartIcon, BarChart3, TrendingUp } from "lucide-react";
interface ResponseTimeChartProps {
uptimeData: UptimeData[];
}
+type ChartType = 'area' | 'line' | 'bar';
+
export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
const { theme } = useTheme();
+ const [chartType, setChartType] = useState('area');
- // Modern color palette for different chart lines with solid colors at 90-100% opacity
- const modernColors = [
- { stroke: '#f59e0b', fill: 'rgba(111, 86, 63, 0.95)' }, // Yellow (changed from blue)
- { stroke: '#10b981', fill: 'rgba(0, 84, 56, 0.95)' }, // Emerald
- { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.95)' }, // Blue (moved to second position)
- { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.95)' }, // Red
- { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.95)' }, // Violet
- { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.95)' }, // Cyan
- { stroke: '#f97316', fill: 'rgba(231, 148, 89, 0.95)' }, // Orange
- { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.95)' }, // Lime
+ // Fixed color palette - consistent colors that don't change
+ const fixedColors = [
+ { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.2)', name: 'Ocean Blue' },
+ { stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.2)', name: 'Emerald Green' },
+ { stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.2)', name: 'Golden Amber' },
+ { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.2)', name: 'Ruby Red' },
+ { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.2)', name: 'Royal Purple' },
+ { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.2)', name: 'Sky Cyan' },
+ { stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.2)', name: 'Sunset Orange' },
+ { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.2)', name: 'Fresh Lime' },
+ { stroke: '#ec4899', fill: 'rgba(236, 72, 153, 0.2)', name: 'Vibrant Pink' },
+ { stroke: '#14b8a6', fill: 'rgba(20, 184, 166, 0.2)', name: 'Ocean Teal' },
];
// Check if we have data from multiple sources
@@ -160,7 +167,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
return [Math.max(0, minValue - padding), maxValue + padding];
}, [chartData, hasMultipleSources]);
- // Get unique sources for legend with proper labeling and colors
+ // Get unique sources for legend with consistent fixed colors
const sources = useMemo(() => {
if (!hasMultipleSources) return [];
@@ -175,25 +182,29 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
}
});
- const regionalSources = Array.from(sourceSet).map((source, index) => {
+ // Sort sources to ensure consistent ordering
+ const sortedSources = Array.from(sourceSet).sort();
+
+ const regionalSources = sortedSources.map((source, index) => {
const data = sourceInfo.get(source);
const [regionName, agentId] = source.split('|');
// Create proper label with region and agent info
let label = regionName;
if (data?.agent_id && data.agent_id !== '1') {
- label = `${regionName} (${data.agent_id})`;
+ label = `${regionName} (Agent ${data.agent_id})`;
} else if (regionName === 'Default' && data?.agent_id === '1') {
- label = `Default System Check (Agent 1)`;
+ label = `Default System Check`;
}
- const colorIndex = index % modernColors.length;
+ const colorIndex = index % fixedColors.length;
return {
key: `regional_${source.replace('|', '_')}`,
label: label,
- stroke: modernColors[colorIndex].stroke,
- fill: modernColors[colorIndex].fill
+ stroke: fixedColors[colorIndex].stroke,
+ fill: fixedColors[colorIndex].fill,
+ colorName: fixedColors[colorIndex].name
};
});
@@ -205,9 +216,10 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
const defaultSources = (hasPureDefault && !hasRegionalDefault) ? [{
key: 'default',
- label: 'Default',
- stroke: modernColors[regionalSources.length % modernColors.length].stroke,
- fill: modernColors[regionalSources.length % modernColors.length].fill
+ label: 'Default System',
+ stroke: fixedColors[regionalSources.length % fixedColors.length].stroke,
+ fill: fixedColors[regionalSources.length % fixedColors.length].fill,
+ colorName: fixedColors[regionalSources.length % fixedColors.length].name
}] : [];
return [...defaultSources, ...regionalSources];
@@ -219,13 +231,13 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
const data = payload[0].payload;
return (
-
-
{String(label)}
-
{String(data.date)}
+
+
{String(label)}
+
{String(data.date)}
{hasMultipleSources ? (
// Multi-source tooltip
-
+
{sources.map(source => {
const valueKey = source.key === 'default' ? 'defaultValue' : `${source.key}_value`;
const statusKey = source.key === 'default' ? 'defaultStatus' : `${source.key}_status`;
@@ -234,35 +246,45 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
if (value === undefined && status === undefined) return null;
- let statusColor = "bg-gray-800";
+ let statusBadgeClass = "px-2 py-1 rounded-full text-xs font-medium";
let statusText = "No Data";
if (status === "up") {
- statusColor = "bg-emerald-800";
+ statusBadgeClass += " bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
statusText = "Up";
} else if (status === "down") {
- statusColor = "bg-red-800";
+ statusBadgeClass += " bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
statusText = "Down";
} else if (status === "warning") {
- statusColor = "bg-yellow-800";
+ statusBadgeClass += " bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
statusText = "Warning";
} else if (status === "paused") {
- statusColor = "bg-gray-800";
+ statusBadgeClass += " bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200";
statusText = "Paused";
+ } else {
+ statusBadgeClass += " bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400";
}
return (
-
-
+
+
-
{String(source.label)}
+
+ {String(source.label)}
+ {source.colorName}
+
-
- {status === "paused" ? "Paused" :
- value !== null && value !== undefined ? `${value} ms` : "No data"}
+
+
+ {statusText}
+
+
+ {status === "paused" ? "Paused" :
+ value !== null && value !== undefined ? `${value} ms` : "No data"}
+
);
@@ -271,19 +293,19 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
) : (
// Single source tooltip - showing status with color
<>
-
-
+
-
{
+ {
data.status === "up" ? "Up" :
data.status === "down" ? "Down" :
data.status === "warning" ? "Warning" : "Paused"
}
-
+
{data.status === "paused" ? "Monitoring paused" :
data.value !== null ? `${data.value} ms` : "No data"}
@@ -295,6 +317,182 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
return null;
};
+ // Render different chart types
+ const renderChart = () => {
+ const commonProps = {
+ data: chartData,
+ margin: { top: 10, right: 30, left: 0, bottom: 30 }
+ };
+
+ const commonAxisProps = {
+ xAxis: {
+ dataKey: "time",
+ stroke: theme === 'dark' ? '#666' : '#9ca3af',
+ angle: -45,
+ textAnchor: "end" as const,
+ tick: { fontSize: 10 },
+ height: 60,
+ interval: "preserveStartEnd" as const,
+ minTickGap: 5
+ },
+ yAxis: {
+ stroke: theme === 'dark' ? '#666' : '#9ca3af',
+ allowDecimals: false,
+ domain: yAxisDomain
+ }
+ };
+
+ if (hasMultipleSources) {
+ switch (chartType) {
+ case 'line':
+ return (
+
+
+
+
+ } />
+
+ {sources.find(s => s.key === 'default') && (
+ s.key === 'default')?.stroke}
+ strokeWidth={3}
+ dot={{ r: 4, strokeWidth: 2 }}
+ connectNulls={false}
+ />
+ )}
+
+ {sources.filter(s => s.key !== 'default').map((source) => (
+
+ ))}
+
+ );
+
+ case 'bar':
+ return (
+
+
+
+
+ } />
+
+ {sources.find(s => s.key === 'default') && (
+ s.key === 'default')?.stroke}
+ opacity={0.8}
+ />
+ )}
+
+ {sources.filter(s => s.key !== 'default').map((source) => (
+
+ ))}
+
+ );
+
+ default: // area
+ return (
+
+
+
+
+ } />
+
+ {sources.find(s => s.key === 'default') && (
+ s.key === 'default')?.stroke}
+ fill={sources.find(s => s.key === 'default')?.fill}
+ strokeWidth={3}
+ dot={false}
+ connectNulls={false}
+ />
+ )}
+
+ {sources.filter(s => s.key !== 'default').map((source) => (
+
+ ))}
+
+ );
+ }
+ } else {
+ // Single source charts remain the same
+ return (
+
+
+
+
+ } />
+
+
+
+
+
+
+
+ {chartData.map((entry, index) =>
+ entry.status === 'paused' ? (
+
+ ) : null
+ )}
+
+ );
+ }
+ };
+
// Check if we have any data to display
const hasData = uptimeData.length > 0;
@@ -319,21 +517,55 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
Response Time History
- {hasData && (
-
- {dateRangeDisplay}
-
- )}
+
+ {hasMultipleSources && (
+
+
setChartType('area')}
+ className="h-8 px-2"
+ >
+
+
+
setChartType('line')}
+ className="h-8 px-2"
+ >
+
+
+
setChartType('bar')}
+ className="h-8 px-2"
+ >
+
+
+
+ )}
+ {hasData && (
+
+ {dateRangeDisplay}
+
+ )}
+
{hasMultipleSources && (
-
+
+
+ Monitoring Sources:
+
{sources.map(source => (
-
+
-
{String(source.label)}
+
{String(source.label)}
+
({source.colorName})
))}
@@ -347,118 +579,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
) : (
- {hasMultipleSources ? (
-
-
-
-
- } />
-
- {/* Default monitoring area with modern solid background */}
- {sources.find(s => s.key === 'default') && (
- s.key === 'default')?.stroke}
- fill={sources.find(s => s.key === 'default')?.fill}
- strokeWidth={2.5}
- dot={false}
- connectNulls={false}
- />
- )}
-
- {/* Regional monitoring areas with modern solid backgrounds */}
- {sources.filter(s => s.key !== 'default').map((source) => (
-
- ))}
-
- ) : (
- // For single regional agent or default monitoring only - use solid background colors
-
-
-
-
- } />
-
- {/* Modern solid background areas for different statuses */}
-
-
-
-
-
-
- {/* Reference lines for paused status */}
- {chartData.map((entry, index) =>
- entry.status === 'paused' ? (
-
- ) : null
- )}
-
- )}
+ {renderChart()}
)}
diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
index 0bc9cc5..b4e6d82 100644
--- a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
+++ b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
@@ -1,5 +1,5 @@
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types";
@@ -18,71 +18,117 @@ export const useRealTimeUpdates = ({
setService,
setUptimeData
}: UseRealTimeUpdatesProps) => {
- // Listen for real-time updates to this service
+ const subscriptionsRef = useRef<{
+ service?: () => void;
+ uptime?: () => void;
+ }>({});
+
+ const lastUpdateRef = useRef
(0);
+ const updateThrottleMs = 30000; // Throttle to once per 30 seconds for better performance
+
+ // Listen for real-time updates to this service with throttling
useEffect(() => {
if (!serviceId) return;
- // console.log(`Setting up real-time updates for service: ${serviceId}`);
+ // console.log(`Setting up real-time updates for service: ${serviceId}`);
- try {
- // Subscribe to the service record for real-time updates
- const subscription = pb.collection('services').subscribe(serviceId, function(e) {
- // console.log("Service updated:", e.record);
-
- // Update our local state with the new data
- if (e.record) {
- setService(prev => {
- if (!prev) return null;
- return {
- ...prev,
- status: e.record.status || prev.status,
- responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
- uptime: e.record.uptime || prev.uptime,
- lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
- };
- });
+ const setupSubscriptions = async () => {
+ try {
+ // Clean up existing subscriptions first
+ if (subscriptionsRef.current.service) {
+ subscriptionsRef.current.service();
+ }
+ if (subscriptionsRef.current.uptime) {
+ subscriptionsRef.current.uptime();
}
- });
- // Subscribe to uptime data updates
- const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) {
- if (e.record && e.record.service_id === serviceId) {
- // console.log("New uptime data:", e.record);
+ // Subscribe to the service record with throttling
+ const serviceUnsubscribe = await pb.collection('services').subscribe(serviceId, function(e) {
+ const now = Date.now();
+ if (now - lastUpdateRef.current < updateThrottleMs) {
+ // console.log("Service update throttled");
+ return;
+ }
+ lastUpdateRef.current = now;
+
+ // console.log("Service updated (throttled):", e.record);
+
+ // Update our local state with the new data
+ if (e.record) {
+ setService(prev => {
+ if (!prev) return null;
+ return {
+ ...prev,
+ status: e.record.status || prev.status,
+ responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
+ uptime: e.record.uptime || prev.uptime,
+ lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
+ };
+ });
+ }
+ });
+
+ subscriptionsRef.current.service = serviceUnsubscribe;
+
+ // Subscribe to uptime data updates with throttling
+ const uptimeUnsubscribe = await pb.collection('uptime_data').subscribe('*', function(e) {
+ if (!e.record || e.record.service_id !== serviceId) return;
+
+ const now = Date.now();
+ if (now - lastUpdateRef.current < updateThrottleMs) {
+ // console.log("Uptime data update throttled");
+ return;
+ }
+ lastUpdateRef.current = now;
+
+ // console.log("New uptime data (throttled):", e.record);
// Add the new uptime data to our list if it's within the selected date range
const timestamp = new Date(e.record.timestamp);
if (timestamp >= startDate && timestamp <= endDate) {
setUptimeData(prev => {
+ // Limit the array size to prevent memory issues
+ const maxRecords = 100;
const newData: UptimeData = {
id: e.record.id,
- service_id: e.record.service_id, // Include service_id
- serviceId: e.record.service_id, // Keep for backward compatibility
+ service_id: e.record.service_id,
+ serviceId: e.record.service_id,
timestamp: e.record.timestamp,
status: e.record.status,
responseTime: e.record.response_time || 0,
- date: e.record.timestamp, // Adding required date property
- uptime: e.record.uptime || 0 // Adding required uptime property
+ date: e.record.timestamp,
+ uptime: e.record.uptime || 0
};
- // Add at the beginning of the array to maintain newest first sorting
- return [newData, ...prev];
+ // Add at the beginning and limit array size
+ const updatedData = [newData, ...prev];
+ return updatedData.slice(0, maxRecords);
});
}
- }
- });
+ });
- // Clean up the subscriptions
- return () => {
- // console.log(`Cleaning up subscriptions for service: ${serviceId}`);
- try {
- pb.collection('services').unsubscribe(serviceId);
- pb.collection('uptime_data').unsubscribe('*');
- } catch (error) {
- // console.error("Error cleaning up subscriptions:", error);
+ subscriptionsRef.current.uptime = uptimeUnsubscribe;
+
+ } catch (error) {
+ // console.error("Error setting up real-time updates:", error);
+ }
+ };
+
+ setupSubscriptions();
+
+ // Return cleanup function
+ return () => {
+ // console.log(`Cleaning up subscriptions for service: ${serviceId}`);
+ try {
+ if (subscriptionsRef.current.service) {
+ subscriptionsRef.current.service();
}
- };
- } catch (error) {
- // console.error("Error setting up real-time updates:", error);
- }
+ if (subscriptionsRef.current.uptime) {
+ subscriptionsRef.current.uptime();
+ }
+ } catch (error) {
+ // console.error("Error cleaning up subscriptions:", error);
+ }
+ };
}, [serviceId, startDate, endDate, setService, setUptimeData]);
};
\ No newline at end of file
diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx
index ccd6259..fa42951 100644
--- a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx
+++ b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx
@@ -1,4 +1,5 @@
-import { useState, useEffect } from "react";
+
+import { useState, useEffect, useCallback, useMemo } from "react";
import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types";
import { useToast } from "@/hooks/use-toast";
@@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const { toast } = useToast();
const navigate = useNavigate();
- // Get regional agents for "all" monitoring
+ // Get regional agents for "all" monitoring with optimized caching
const { data: regionalAgents = [] } = useQuery({
queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices,
- enabled: selectedRegionalAgent === "all"
+ enabled: selectedRegionalAgent === "all",
+ staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+ gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
+ refetchOnWindowFocus: false,
});
- const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
+ const handleStatusChange = useCallback(async (newStatus: "up" | "down" | "paused" | "warning") => {
if (!service || !serviceId) return;
try {
@@ -38,7 +42,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
description: `Service status changed to ${newStatus}`,
});
} catch (error) {
- // console.error("Failed to update service status:", error);
+ // console.error("Failed to update service status:", error);
setService(prevService => prevService);
toast({
@@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
description: "Could not update service status. Please try again.",
});
}
- };
+ }, [service, serviceId, toast]);
- const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
+ const fetchUptimeData = useCallback(async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
try {
if (!service) {
// console.log('No service data available for uptime fetch');
@@ -80,7 +84,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// Fetch default monitoring data
const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
- // console.log(`Retrieved ${defaultData.length} default monitoring records`);
+ // console.log(`Retrieved ${defaultData.length} default monitoring records`);
// Mark default data with source identifier
const markedDefaultData = defaultData.map(record => ({
@@ -100,7 +104,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const regionalData = await uptimeService.getUptimeHistoryByRegionalAgent(
serviceId, limit, start, end, service.type, agent.region_name, agent.agent_id
);
- // console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
+ // console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
// Mark regional data with source identifier
const markedRegionalData = regionalData.map(record => ({
@@ -112,18 +116,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
history = [...history, ...markedRegionalData];
} catch (error) {
- // console.error(`Error fetching data from ${agent.region_name}:`, error);
+ // console.error(`Error fetching data from ${agent.region_name}:`, error);
}
}
- // console.log(`Total combined records: ${history.length}`);
+ // console.log(`Total combined records: ${history.length}`);
} else {
// Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|");
- // console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
+ console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
- // console.log(`Retrieved ${history.length} regional monitoring records`);
+ // console.log(`Retrieved ${history.length} regional monitoring records`);
}
// Sort by timestamp (newest first)
@@ -131,7 +135,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
- //console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
+ // console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
setUptimeData(filteredHistory);
return filteredHistory;
} catch (error) {
@@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
});
return [];
}
- };
+ }, [service, selectedRegionalAgent, regionalAgents, toast]);
- const handleRegionalAgentChange = (agent: string) => {
+ const handleRegionalAgentChange = useCallback((agent: string) => {
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
// Clear data immediately when switching
@@ -154,78 +158,83 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// Refetch data with new agent selection
if (serviceId && !isLoading && service) {
- // console.log(`Refetching data for new agent: ${agent}`);
+ // console.log(`Refetching data for new agent: ${agent}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
}
- };
+ }, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]);
+
+ // Memoize the service data fetching to prevent unnecessary re-runs
+ const fetchServiceData = useCallback(async () => {
+ try {
+ if (!serviceId) {
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+
+ const timeoutPromise = new Promise((_, reject) => {
+ setTimeout(() => reject(new Error("Request timed out")), 10000);
+ });
+
+ const fetchPromise = pb.collection('services').getOne(serviceId);
+ const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
+
+ const formattedService: Service = {
+ id: serviceData.id,
+ name: serviceData.name,
+ url: serviceData.url || "",
+ host: serviceData.host || "",
+ port: serviceData.port || undefined,
+ domain: serviceData.domain || "",
+ type: serviceData.service_type || serviceData.type || "HTTP",
+ status: serviceData.status || "paused",
+ responseTime: serviceData.response_time || serviceData.responseTime || 0,
+ uptime: serviceData.uptime || 0,
+ lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
+ interval: serviceData.heartbeat_interval || serviceData.interval || 60,
+ retries: serviceData.max_retries || serviceData.retries || 3,
+ notificationChannel: serviceData.notification_id,
+ alertTemplate: serviceData.template_id,
+ alerts: serviceData.alerts || "unmuted"
+ };
+
+ // console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
+ setService(formattedService);
+
+ // Small delay to ensure state is updated before fetching uptime data
+ await new Promise(resolve => setTimeout(resolve, 100));
+ } catch (error) {
+ // console.error("Error fetching service:", error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to load service data. Please try again.",
+ });
+ navigate("/dashboard");
+ } finally {
+ setIsLoading(false);
+ }
+ }, [serviceId, navigate, toast]);
// Initial data loading
useEffect(() => {
- const fetchServiceData = async () => {
- try {
- if (!serviceId) {
- setIsLoading(false);
- return;
- }
-
- setIsLoading(true);
-
- const timeoutPromise = new Promise((_, reject) => {
- setTimeout(() => reject(new Error("Request timed out")), 10000);
- });
-
- const fetchPromise = pb.collection('services').getOne(serviceId);
- const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
-
- const formattedService: Service = {
- id: serviceData.id,
- name: serviceData.name,
- url: serviceData.url || "",
- host: serviceData.host || "",
- port: serviceData.port || undefined,
- domain: serviceData.domain || "",
- type: serviceData.service_type || serviceData.type || "HTTP",
- status: serviceData.status || "paused",
- responseTime: serviceData.response_time || serviceData.responseTime || 0,
- uptime: serviceData.uptime || 0,
- lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
- interval: serviceData.heartbeat_interval || serviceData.interval || 60,
- retries: serviceData.max_retries || serviceData.retries || 3,
- notificationChannel: serviceData.notification_id,
- alertTemplate: serviceData.template_id,
- alerts: serviceData.alerts || "unmuted"
- };
-
- // console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
- setService(formattedService);
-
- // Small delay to ensure state is updated before fetching uptime data
- await new Promise(resolve => setTimeout(resolve, 100));
- } catch (error) {
- // console.error("Error fetching service:", error);
- toast({
- variant: "destructive",
- title: "Error",
- description: "Failed to load service data. Please try again.",
- });
- navigate("/dashboard");
- } finally {
- setIsLoading(false);
- }
- };
-
fetchServiceData();
- }, [serviceId, navigate, toast]);
+ }, [fetchServiceData]);
- // Update data when date range changes or when service is loaded
+ // Update data when date range changes or when service is loaded - with debouncing
useEffect(() => {
if (serviceId && !isLoading && service) {
- // console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
- fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
- }
- }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
+ const timeoutId = setTimeout(() => {
+ // console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
+ fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
+ }, 500); // Debounce API calls by 500ms
- return {
+ return () => clearTimeout(timeoutId);
+ }
+ }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]);
+
+ return useMemo(() => ({
service,
setService,
uptimeData,
@@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
fetchUptimeData,
selectedRegionalAgent,
handleRegionalAgentChange
- };
+ }), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]);
};
\ No newline at end of file
diff --git a/application/src/components/services/ServiceDetailContent.tsx b/application/src/components/services/ServiceDetailContent.tsx
index d925312..8c26143 100644
--- a/application/src/components/services/ServiceDetailContent.tsx
+++ b/application/src/components/services/ServiceDetailContent.tsx
@@ -1,4 +1,3 @@
-
import { Service, UptimeData } from "@/types/service.types";
import { ServiceHeader } from "@/components/services/ServiceHeader";
import { ServiceStatsCards } from "@/components/services/ServiceStatsCards";
@@ -37,6 +36,7 @@ export const ServiceDetailContent = ({
onStatusChange={onStatusChange}
selectedRegionalAgent={selectedRegionalAgent}
onRegionalAgentChange={onRegionalAgentChange}
+ uptimeData={uptimeData}
/>
diff --git a/application/src/components/services/ServiceHeader.tsx b/application/src/components/services/ServiceHeader.tsx
index 5016297..9bf2b15 100644
--- a/application/src/components/services/ServiceHeader.tsx
+++ b/application/src/components/services/ServiceHeader.tsx
@@ -1,93 +1,116 @@
-import { ArrowLeft, Globe } from "lucide-react";
+import { ArrowLeft, Globe, BarChart3 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/services/StatusBadge";
import { ServiceMonitoringButton } from "@/components/services/ServiceMonitoringButton";
import { RegionalAgentFilter } from "@/components/services/RegionalAgentFilter";
-import { Service } from "@/types/service.types";
+import { HeatmapDialog } from "./HeatmapDialog";
+import { Service, UptimeData } from "@/types/service.types";
import { useLanguage } from "@/contexts/LanguageContext";
import { cn } from "@/lib/utils";
+import { useState } from "react";
interface ServiceHeaderProps {
service: Service;
onStatusChange?: (newStatus: "up" | "down" | "paused" | "warning") => void;
selectedRegionalAgent?: string;
onRegionalAgentChange?: (agent: string) => void;
+ uptimeData?: UptimeData[];
}
export function ServiceHeader({
service,
onStatusChange,
selectedRegionalAgent,
- onRegionalAgentChange
+ onRegionalAgentChange,
+ uptimeData = []
}: ServiceHeaderProps) {
const navigate = useNavigate();
const { t } = useLanguage();
+ const [showHeatmap, setShowHeatmap] = useState(false);
return (
-
-
navigate("/dashboard")}
- >
-
- {t("back")}
-
-
-
-
-
{service.name}
-
- {/* Pulsating Circle Animation */}
-
-
-
+ <>
+
+
navigate("/dashboard")}
+ >
+
+ {t("back")}
+
+
+
-
-
-
-
- {selectedRegionalAgent !== undefined && onRegionalAgentChange && (
-
- )}
+
+ Heatmap
+
+ {selectedRegionalAgent !== undefined && onRegionalAgentChange && (
+
+ )}
+
-
+
+
+ >
);
}
\ No newline at end of file
diff --git a/application/src/components/services/StatusBadge.tsx b/application/src/components/services/StatusBadge.tsx
index 2621b0a..df8b8de 100644
--- a/application/src/components/services/StatusBadge.tsx
+++ b/application/src/components/services/StatusBadge.tsx
@@ -1,71 +1,70 @@
-import React from "react";
-import { Check, X, Pause, AlertTriangle } from "lucide-react";
+import React, { memo } from "react";
+import { Badge } from "@/components/ui/badge";
+import { Check } from "lucide-react";
-export interface StatusBadgeProps {
- status: string;
+interface StatusBadgeProps {
+ status: "up" | "down" | "paused" | "warning";
size?: "sm" | "md" | "lg";
}
-export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => {
- // Determine the sizing classes based on the size prop
- const getSizeClasses = () => {
- switch (size) {
- case "lg":
- return "px-3 py-1.5 text-sm gap-1.5";
- case "md":
- return "px-2.5 py-1 text-sm gap-1.5";
- case "sm":
+const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => {
+ const getStatusConfig = (status: string) => {
+ switch (status) {
+ case "up":
+ return {
+ variant: "default" as const,
+ className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200",
+ label:
+
+ Up
+
+
+ };
+ case "down":
+ return {
+ variant: "destructive" as const,
+ className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200",
+ label: "Down"
+ };
+ case "warning":
+ return {
+ variant: "destructive" as const,
+ className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200",
+ label: "Warning"
+ };
+ case "paused":
+ return {
+ variant: "secondary" as const,
+ className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200",
+ label: "Paused"
+ };
default:
- return "px-2 py-0.5 text-xs gap-0.5";
+ return {
+ variant: "outline" as const,
+ className: "bg-gray-700 text-gray-100 border-gray-200",
+ label: "Unknown"
+ };
}
};
- const getIconSize = () => {
- switch (size) {
- case "lg":
- return "h-4 w-4";
- case "md":
- return "h-4 w-4";
- case "sm":
- default:
- return "h-3 w-3";
- }
+ const sizeClasses = {
+ sm: "text-xs px-2 py-1",
+ md: "text-sm px-3 py-1.5",
+ lg: "text-base px-4 py-2"
};
- const sizeClasses = getSizeClasses();
- const iconSize = getIconSize();
+ const config = getStatusConfig(status);
- switch (status) {
- case "up":
- return (
-
-
- Up
-
- );
- case "down":
- return (
-
-
- Down
-
- );
- case "warning":
- return (
-
- );
- case "paused":
- return (
-
- );
- default:
- return null;
- }
+ return (
+
+ {config.label}
+
+ );
};
+
+// Memoize the component to prevent unnecessary re-renders
+export const StatusBadge = memo(StatusBadgeComponent);
\ No newline at end of file
diff --git a/application/src/components/services/UptimeBar.tsx b/application/src/components/services/UptimeBar.tsx
index 1a827fd..642e35e 100644
--- a/application/src/components/services/UptimeBar.tsx
+++ b/application/src/components/services/UptimeBar.tsx
@@ -1,9 +1,10 @@
-import React from "react";
-import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
-import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
-import { UptimeSummary } from "./uptime/UptimeSummary";
-import { formatRelative } from "date-fns";
+import React, { memo, useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { TooltipProvider } from '@/components/ui/tooltip';
+import { UptimeStatusItem } from './uptime/UptimeStatusItem';
+import { uptimeService } from '@/services/uptimeService';
+import { UptimeData } from '@/types/service.types';
interface UptimeBarProps {
uptime: number;
@@ -13,127 +14,81 @@ interface UptimeBarProps {
serviceType?: string;
}
-export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => {
- // Use consolidated hook to get properly merged data
- const { consolidatedItems, isLoading } = useConsolidatedUptimeData({
- serviceId,
- serviceType,
- status,
- interval
+const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
+ // Calculate date range for last 20 checks with much more aggressive caching
+ const endDate = useMemo(() => new Date(), []);
+ const startDate = useMemo(() => {
+ const start = new Date(endDate);
+ start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours
+ return start;
+ }, [endDate, interval]);
+
+ // Fetch uptime data with very aggressive caching to reduce API calls
+ const { data: uptimeData = [] } = useQuery({
+ queryKey: ['uptime-bar', serviceId, serviceType],
+ queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
+ enabled: !!serviceId,
+ staleTime: 30000, // Data is fresh for 30 seconds
+ gcTime: 600000, // Keep in cache for 10 minutes
+ refetchInterval: 60000, // 1 minute polling
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ retry: 3,
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
- const getStatusColor = (itemStatus: string, hasData: boolean = true) => {
- if (!hasData) {
- return "bg-gray-400"; // No data color - grey
+ // Memoize the uptime calculation to prevent unnecessary recalculations
+ const { uptimePercentage, displayData } = useMemo(() => {
+ if (!uptimeData || uptimeData.length === 0) {
+ return {
+ uptimePercentage: uptime || 0,
+ displayData: []
+ };
}
-
- switch (itemStatus) {
- case "up":
- return "bg-emerald-500";
- case "down":
- return "bg-red-500";
- case "warning":
- return "bg-yellow-500";
- case "paused":
- return "bg-gray-400"; // Paused status - grey
- case "unknown":
- case "NA":
- default:
- return "bg-gray-400"; // Unknown/NA/default - grey
- }
- };
- if (isLoading) {
- return (
-
-
- {Array(20).fill(null).map((_, index) => (
-
- ))}
-
-
-
- );
- }
+ // Calculate uptime percentage from actual data
+ const upCount = uptimeData.filter(item => item.status === 'up').length;
+ const totalCount = uptimeData.length;
+ const calculatedUptime = totalCount > 0 ? (upCount / totalCount) * 100 : 0;
- // console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
+ // Limit display to last 20 items for performance
+ const limitedData = uptimeData.slice(0, 20);
+
+ return {
+ uptimePercentage: Math.round(calculatedUptime * 100) / 100,
+ displayData: limitedData
+ };
+ }, [uptimeData, uptime]);
+
+ // Memoize the status items to prevent unnecessary re-renders
+ const statusItems = useMemo(() =>
+ displayData.map((item, index) => (
+
+ )),
+ [displayData]
+ );
return (
-
-
-
- {consolidatedItems.map((slot, index) => {
- // Check if we have actual monitoring data with valid status
- const hasValidData = slot.items.length > 0 && slot.items.some(item =>
- item.status && ['up', 'down', 'warning', 'paused'].includes(item.status)
- );
-
- // Determine the primary status for bar color (prioritize worst status)
- let primaryStatus = 'unknown';
- if (hasValidData) {
- const statuses = slot.items.map(item => item.status);
- primaryStatus = statuses.includes('down') ? 'down' :
- statuses.includes('warning') ? 'warning' :
- statuses.includes('up') ? 'up' :
- statuses.includes('paused') ? 'paused' : 'unknown';
- }
-
- // console.log(`Bar ${index} - Timestamp: ${slot.timestamp}, Items: ${slot.items.length}, Primary Status: ${primaryStatus}, Has Valid Data: ${hasValidData}`);
- slot.items.forEach((item, itemIndex) => {
- // console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}, IsDefault=${item.isDefault}`);
- });
-
- return (
-
-
-
-
-
-
-
- {formatRelative(new Date(slot.timestamp), new Date())}
-
- {hasValidData ? (
-
- {slot.items.map((item, itemIndex) => (
-
-
-
- {item.status === 'paused' ? 'Paused' :
- item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` :
- 'No response'}
-
-
- ))}
- {slot.items.length > 1 && (
-
- {slot.items.length} monitoring sources
-
- )}
-
- ) : (
-
- No monitoring data available
-
- )}
-
-
-
- );
- })}
+
+
+
+ {statusItems.length > 0 ? statusItems : (
+ // Fallback display when no data
+
+ )}
-
-
-
+
+ {uptimePercentage.toFixed(1)}%
+
+
+
+
+ Last 20 checks
+
+
);
-};
\ No newline at end of file
+};
+
+// Memoize the component to prevent unnecessary re-renders
+export const UptimeBar = memo(UptimeBarComponent);
\ No newline at end of file
diff --git a/application/src/components/services/add-service/ServiceForm.tsx b/application/src/components/services/add-service/ServiceForm.tsx
index 8e66a92..a888b11 100644
--- a/application/src/components/services/add-service/ServiceForm.tsx
+++ b/application/src/components/services/add-service/ServiceForm.tsx
@@ -13,6 +13,7 @@ import { serviceService } from "@/services/serviceService";
import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./ServiceRegionalFields";
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
+import { useQueryClient } from "@tanstack/react-query";
interface ServiceFormProps {
onSuccess: () => void;
@@ -31,6 +32,7 @@ export function ServiceForm({
}: ServiceFormProps) {
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
+ const queryClient = useQueryClient();
// Initialize form with default values
const form = useForm
({
@@ -68,10 +70,10 @@ export function ServiceForm({
if (onSubmitStart) onSubmitStart();
try {
- console.log("Form data being submitted:", data);
+ // console.log("Form data being submitted:", data);
const serviceData = mapFormDataToServiceData(data);
- console.log("Service data being sent:", serviceData);
+ // console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -91,6 +93,16 @@ export function ServiceForm({
});
}
+ // Force immediate refresh of services data
+ queryClient.setQueryData(["services"], (oldData: any) => {
+ // Invalidate the cache to force a fresh fetch
+ return undefined;
+ });
+
+ // Invalidate and refetch services query
+ await queryClient.invalidateQueries({ queryKey: ["services"] });
+ await queryClient.refetchQueries({ queryKey: ["services"] });
+
onSuccess();
if (!isEdit) {
form.reset();
diff --git a/application/src/components/services/add-service/ServiceUrlField.tsx b/application/src/components/services/add-service/ServiceUrlField.tsx
index 0284f35..f8419c4 100644
--- a/application/src/components/services/add-service/ServiceUrlField.tsx
+++ b/application/src/components/services/add-service/ServiceUrlField.tsx
@@ -67,7 +67,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
placeholder={getPlaceholder()}
{...field}
onChange={(e) => {
- console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
+ // console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
field.onChange(e);
}}
/>
@@ -93,7 +93,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
type="number"
{...field}
onChange={(e) => {
- console.log("Port field changed:", e.target.value);
+ // console.log("Port field changed:", e.target.value);
field.onChange(e);
}}
/>
diff --git a/application/src/components/services/hooks/useUptimeData.ts b/application/src/components/services/hooks/useUptimeData.ts
index 0bfe235..96a20d1 100644
--- a/application/src/components/services/hooks/useUptimeData.ts
+++ b/application/src/components/services/hooks/useUptimeData.ts
@@ -14,7 +14,7 @@ interface UseUptimeDataProps {
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
const [historyItems, setHistoryItems] = useState([]);
- // Fetch ALL uptime history data including regional monitoring data
+ // Fetch ALL uptime history data including regional monitoring data with 1-minute polling
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['allUptimeHistory', serviceId, serviceType],
queryFn: async () => {
@@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
return allMonitoringData;
},
enabled: !!serviceId,
- refetchInterval: 30000,
- staleTime: 15000,
+ refetchInterval: 60000, // 1 minute polling
+ staleTime: 30000, // Data is fresh for 30 seconds
placeholderData: (previousData) => previousData,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
diff --git a/application/src/components/settings/about-system/AboutSystem.tsx b/application/src/components/settings/about-system/AboutSystem.tsx
index 108cc32..b06e7d8 100644
--- a/application/src/components/settings/about-system/AboutSystem.tsx
+++ b/application/src/components/settings/about-system/AboutSystem.tsx
@@ -40,7 +40,6 @@ export const AboutSystem: React.FC = () => {
{t('aboutSystem')}
- {t('aboutCheckcle')}
diff --git a/application/src/components/ssl-domain/SSLCertificateActions.tsx b/application/src/components/ssl-domain/SSLCertificateActions.tsx
index a1a2148..68ad461 100644
--- a/application/src/components/ssl-domain/SSLCertificateActions.tsx
+++ b/application/src/components/ssl-domain/SSLCertificateActions.tsx
@@ -45,20 +45,20 @@ export const SSLCertificateActions = ({
- onView(certificate)}>
+ { e.stopPropagation(); onView(certificate); }}>
{t('view')}
-
+ { e.stopPropagation(); handleCheck(); }}>
Check
- onEdit(certificate)}>
+ { e.stopPropagation(); onEdit(certificate); }}>
{t('edit')}
onDelete(certificate)}
+ onClick={(e) => { e.stopPropagation(); onDelete(certificate); }}
className="text-destructive"
>
@@ -67,4 +67,4 @@ export const SSLCertificateActions = ({
);
-};
\ No newline at end of file
+};
diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
index ce6fb15..dd28818 100644
--- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx
+++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
@@ -1,8 +1,8 @@
+
import React, { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
@@ -37,10 +37,12 @@ import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@
import { pb } from "@/lib/pocketbase";
import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext";
+import { useTheme } from "@/contexts/ThemeContext";
import { toast } from "sonner";
export const SSLCertificatesTable = () => {
const { t } = useLanguage();
+ const { theme } = useTheme();
const queryClient = useQueryClient();
const [showAddDialog, setShowAddDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
@@ -65,7 +67,7 @@ export const SSLCertificatesTable = () => {
setShowAddDialog(false);
toast.success(t('certificateAdded'));
} catch (error) {
- console.error("Error adding certificate:", error);
+ // console.error("Error adding certificate:", error);
toast.error(t('failedToAddCertificate'));
} finally {
setIsSubmitting(false);
@@ -87,7 +89,7 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateUpdated'));
} catch (error) {
- console.error("Error updating certificate:", error);
+ // console.error("Error updating certificate:", error);
toast.error(t('failedToUpdateCertificate'));
} finally {
setIsSubmitting(false);
@@ -105,7 +107,7 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateDeleted'));
} catch (error) {
- console.error("Error deleting certificate:", error);
+ // console.error("Error deleting certificate:", error);
toast.error(t('failedToDeleteCertificate'));
} finally {
setIsSubmitting(false);
@@ -128,24 +130,32 @@ export const SSLCertificatesTable = () => {
};
return (
- <>
-
-
+
+ {certificates.length === 0 ? (
+
+ {t('noCertificatesFound')}
+
+ ) : (
+
-
-
- {t('domain')}
- {t('status')}
- {t('issuer')}
- {t('validUntil')}
- {t('daysLeft')}
- Check Interval
-
+
+
+ {t('domain')}
+ {t('status')}
+ {t('issuer')}
+ {t('validUntil')}
+ {t('daysLeft')}
+ Check Interval
+ Actions
{certificates.map((certificate) => (
-
+ openViewDialog(certificate)}
+ >
{certificate.domain}
@@ -164,7 +174,7 @@ export const SSLCertificatesTable = () => {
{certificate.check_interval || 1} {t('days')}
-
+ e.stopPropagation()}>
{
))}
-
- {certificates.length === 0 && (
-
- {t('noCertificatesFound')}
-
- )}
-
-
+
+ )}
{/* View Certificate Dialog */}
{
- >
+
);
-};
\ No newline at end of file
+};
diff --git a/application/src/components/ssl-domain/SSLDomainContent.tsx b/application/src/components/ssl-domain/SSLDomainContent.tsx
index 6769462..bb38980 100644
--- a/application/src/components/ssl-domain/SSLDomainContent.tsx
+++ b/application/src/components/ssl-domain/SSLDomainContent.tsx
@@ -30,12 +30,12 @@ export const SSLDomainContent = () => {
queryKey: ['ssl-certificates'],
queryFn: async () => {
try {
- // console.log("Fetching SSL certificates from SSLDomainContent...");
+ console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates();
- // console.log("Received SSL certificates:", result);
+ console.log("Received SSL certificates:", result);
return result;
} catch (error) {
- // console.error("Error fetching certificates:", error);
+ console.error("Error fetching certificates:", error);
toast.error(t('failedToLoadCertificates'));
throw error;
}
@@ -53,7 +53,7 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateAdded'));
},
onError: (error) => {
- // console.error("Error adding SSL certificate:", error);
+ console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
}
});
@@ -61,7 +61,7 @@ export const SSLDomainContent = () => {
// Edit certificate mutation - Updated to ensure thresholds are properly updated
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
- // console.log("Updating certificate with data:", certificate);
+ console.log("Updating certificate with data:", certificate);
// Create the update data object
const updateData = {
@@ -70,12 +70,12 @@ export const SSLDomainContent = () => {
notification_channel: certificate.notification_channel,
};
- // console.log("Update data to be sent:", updateData);
+ console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
- // console.log("PocketBase update response:", updated);
+ console.log("PocketBase update response:", updated);
// After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds
@@ -90,7 +90,7 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateUpdated'));
},
onError: (error) => {
- // console.error("Error updating SSL certificate:", error);
+ console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
});
@@ -103,33 +103,26 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateDeleted'));
},
onError: (error) => {
- // console.error("Error deleting SSL certificate:", error);
+ console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
});
- // Refresh certificate mutation - Updated to ensure notifications are sent
+ // Refresh certificate mutation - Updated to remove individual toast notifications
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null);
- toast.success(t('sslCertificateRefreshed').replace('{domain}', data.domain));
+ // Removed individual success toast notification
},
onError: (error) => {
- // console.error("Error refreshing SSL certificate:", error);
-
- let errorMessage = t('failedToCheckCertificate');
-
- if (error instanceof Error) {
- errorMessage = error.message;
- }
-
- toast.error(errorMessage);
+ console.error("Error refreshing SSL certificate:", error);
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ // Removed individual error toast notification
}
});
@@ -149,7 +142,7 @@ export const SSLDomainContent = () => {
}
},
onError: (error) => {
- // console.error("Error refreshing all certificates:", error);
+ console.error("Error refreshing all certificates:", error);
toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false);
@@ -174,7 +167,7 @@ export const SSLDomainContent = () => {
};
const handleUpdateCertificate = (certificate: SSLCertificate) => {
- // console.log("Handling certificate update with data:", certificate);
+ console.log("Handling certificate update with data:", certificate);
editMutation.mutate(certificate);
};
diff --git a/application/src/pages/ContainerMonitoring.tsx b/application/src/pages/ContainerMonitoring.tsx
index c302806..b5090d4 100644
--- a/application/src/pages/ContainerMonitoring.tsx
+++ b/application/src/pages/ContainerMonitoring.tsx
@@ -29,7 +29,7 @@ const ContainerMonitoring = () => {
});
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
- console.log('ContainerMonitoring component loaded with serverId:', serverId);
+ // console.log('ContainerMonitoring component loaded with serverId:', serverId);
const {
data: containers = [],
@@ -39,26 +39,26 @@ const ContainerMonitoring = () => {
} = useQuery({
queryKey: ['docker-containers', serverId],
queryFn: () => {
- console.log('Query function called with serverId:', serverId);
+ // console.log('Query function called with serverId:', serverId);
return serverId ? dockerService.getContainersByServerId(serverId) : dockerService.getContainers();
},
refetchInterval: 30000 // Refetch every 30 seconds
});
- console.log('Query state:', { containers, isLoading, error });
+ // console.log('Query state:', { containers, isLoading, error });
useEffect(() => {
- console.log('Containers changed:', containers);
+ // console.log('Containers changed:', containers);
if (containers.length > 0) {
dockerService.getContainerStats(containers).then(newStats => {
- console.log('Stats calculated:', newStats);
+ // console.log('Stats calculated:', newStats);
setStats(newStats);
});
}
}, [containers]);
const handleRefresh = () => {
- console.log('Manual refresh triggered');
+ // console.log('Manual refresh triggered');
refetch();
};
@@ -72,7 +72,7 @@ const ContainerMonitoring = () => {
};
if (error) {
- console.error('Container monitoring error:', error);
+ // console.error('Container monitoring error:', error);
return (
diff --git a/application/src/pages/Dashboard.tsx b/application/src/pages/Dashboard.tsx
index a61dd8e..2ab2c52 100644
--- a/application/src/pages/Dashboard.tsx
+++ b/application/src/pages/Dashboard.tsx
@@ -20,7 +20,7 @@ const Dashboard = () => {
// For debugging user data
useEffect(() => {
- // console.log("Current user data:", currentUser);
+ // console.log("Current user data:", currentUser);
}, [currentUser]);
// Handle logout
@@ -29,22 +29,39 @@ const Dashboard = () => {
navigate("/login");
};
- // Fetch all services
+ // Fetch all services with 1-minute polling for real-time updates
const { data: services = [], isLoading, error } = useQuery({
queryKey: ['services'],
queryFn: serviceService.getServices,
- refetchInterval: 10000, // Refresh data every 10 seconds
+ refetchInterval: 60000, // 1 minute as requested
+ staleTime: 30000, // Data is fresh for 30 seconds
+ gcTime: 120000, // Keep in cache for 2 minutes
+ refetchOnWindowFocus: true, // Refetch when window gains focus
+ refetchOnMount: true, // Refetch on mount
+ refetchOnReconnect: true, // Refetch on reconnect
+ retry: 2,
+ retryDelay: 3000,
});
- // Start monitoring all active services when the dashboard loads
+ // Start monitoring all active services when the dashboard loads - only once
useEffect(() => {
+ let hasStarted = false;
+
const startActiveServices = async () => {
+ if (hasStarted) return;
+ hasStarted = true;
+
await serviceService.startAllActiveServices();
- // console.log("Active services monitoring started");
+ // console.log("Active services monitoring started");
};
- startActiveServices();
- }, []);
+ // Only start once and add a delay to prevent immediate execution
+ const timeoutId = setTimeout(startActiveServices, 2000);
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, []); // Remove services dependency to prevent re-runs
// Show the loading state while fetching data
if (isLoading) {
diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx
index 7ad1fc4..8ab5641 100644
--- a/application/src/pages/InstanceMonitoring.tsx
+++ b/application/src/pages/InstanceMonitoring.tsx
@@ -7,11 +7,14 @@ import { Sidebar } from "@/components/dashboard/Sidebar";
import { Header } from "@/components/dashboard/Header";
import { ServerStatsCards } from "@/components/servers/ServerStatsCards";
import { ServerTable } from "@/components/servers/ServerTable";
+import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog";
import { serverService } from "@/services/serverService";
import { Server, ServerStats } from "@/types/server.types";
import { useSidebar } from "@/contexts/SidebarContext";
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import { Plus } from "lucide-react";
const InstanceMonitoring = () => {
const { theme } = useTheme();
@@ -27,6 +30,7 @@ const InstanceMonitoring = () => {
});
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
+ const [addDialogOpen, setAddDialogOpen] = useState(false);
const { data: servers = [], isLoading, error, refetch } = useQuery({
queryKey: ['servers'],
@@ -48,12 +52,16 @@ const InstanceMonitoring = () => {
authService.logout();
navigate('/login');
};
+
+ const handleAgentAdded = () => {
+ refetch();
+ };
if (error) {
return (
-
+
{
return (
-
+
{
toggleSidebar={toggleSidebar}
/>
-
+
{/* Header Section */}
-
+
-
+
Instance Monitoring
-
+
Monitor and manage your server instances in real-time
+
setAddDialogOpen(true)} className="flex-shrink-0">
+
+ Add Server Agent
+
{/* Stats Cards Section */}
-
+
{/* Server Table Section */}
-
+
+
);
};
diff --git a/application/src/pages/ServerDetail.tsx b/application/src/pages/ServerDetail.tsx
index 3dee273..9b714e7 100644
--- a/application/src/pages/ServerDetail.tsx
+++ b/application/src/pages/ServerDetail.tsx
@@ -1,4 +1,3 @@
-
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
@@ -10,8 +9,7 @@ import { Header } from "@/components/dashboard/Header";
import { serverService } from "@/services/serverService";
import { authService } from "@/services/authService";
import { Button } from "@/components/ui/button";
-import { ArrowLeft, Server, Database } from "lucide-react";
-import { ServerMetricsCharts } from "@/components/servers/ServerMetricsCharts";
+import { ArrowLeft, Server } from "lucide-react";
import { ServerMetricsOverview } from "@/components/servers/ServerMetricsOverview";
import { ServerHistoryCharts } from "@/components/servers/ServerHistoryCharts";
import { ServerSystemInfoCard } from "@/components/servers/ServerSystemInfoCard";
@@ -36,6 +34,70 @@ const ServerDetail = () => {
enabled: !!serverId
});
+ const getOSLogo = (server: any) => {
+ if (!server) return null;
+
+ // Parse system_info if it's a string, but handle both JSON and plain text
+ let systemInfo: any = {};
+ let systemInfoText = '';
+
+ if (server.system_info) {
+ if (typeof server.system_info === 'string') {
+ // Try to parse as JSON first
+ try {
+ systemInfo = JSON.parse(server.system_info);
+ } catch (error) {
+ // If JSON parsing fails, treat it as plain text
+ // console.log('system_info is plain text:', server.system_info);
+ systemInfoText = server.system_info.toLowerCase();
+ }
+ } else {
+ systemInfo = server.system_info;
+ }
+ }
+
+ // Check system_info (both JSON and plain text), then fallback to os_type
+ const osFromJson = systemInfo.OSName || '';
+ const osFromText = systemInfoText;
+ const osFromType = server.os_type || '';
+
+ // Combine all OS information for detection
+ const combinedOSInfo = `${osFromJson} ${osFromText} ${osFromType}`.toLowerCase();
+
+ // console.log('OS detection info:', { osFromJson, osFromText, osFromType, combinedOSInfo });
+
+ // Check for specific OS distributions first (most specific to least specific)
+ if (combinedOSInfo.includes('ubuntu')) {
+ return '/upload/os/ubuntu.png';
+ } else if (combinedOSInfo.includes('debian')) {
+ return '/upload/os/debian.png';
+ } else if (combinedOSInfo.includes('centos')) {
+ return '/upload/os/centos.png';
+ } else if (combinedOSInfo.includes('rhel') || combinedOSInfo.includes('red hat')) {
+ return '/upload/os/rhel.png';
+ } else if (combinedOSInfo.includes('fedora')) {
+ return '/upload/os/fedora.png';
+ } else if (combinedOSInfo.includes('suse') || combinedOSInfo.includes('opensuse')) {
+ return '/upload/os/suse.png';
+ } else if (combinedOSInfo.includes('arch')) {
+ return '/upload/os/arch.png';
+ } else if (combinedOSInfo.includes('alpine')) {
+ return '/upload/os/alpine.png';
+ } else if (combinedOSInfo.includes('windows')) {
+ return '/upload/os/windows.png';
+ } else if (combinedOSInfo.includes('macos') || combinedOSInfo.includes('darwin') || combinedOSInfo.includes('mac os')) {
+ return '/upload/os/macos.png';
+ } else if (combinedOSInfo.includes('freebsd')) {
+ return '/upload/os/freebsd.png';
+ } else if (combinedOSInfo.includes('linux') || combinedOSInfo.includes('gnu')) {
+ // Default to linux.png for any Linux-based system that doesn't match specific distributions
+ return '/upload/os/linux.png';
+ }
+
+ // Final fallback - if we can't determine the OS, default to linux.png
+ return '/upload/os/linux.png';
+ };
+
const handleLogout = () => {
authService.logout();
navigate('/login');
@@ -46,7 +108,6 @@ const ServerDetail = () => {
};
if (serverError) {
- // console.error('Server detail error:', serverError);
return (
@@ -101,7 +162,7 @@ const ServerDetail = () => {
return (
-
+
-
-
+
+ {server && getOSLogo(server) ? (
+
+ ) : (
+
+ )}
{server?.name || 'Server Detail'}
-
+
Monitor server performance metrics and system health
{server && (
-
+
{server.hostname} β’ {server.ip_address} β’ {server.os_type}
-
)}
- {/* System Info Card */}
+
+ {/* System Info Card */}
{server && (
@@ -159,17 +228,10 @@ const ServerDetail = () => {
)}
- {/* Historical Charts Section */}
- {server && (
-
-
-
- )}
-
- {/* Metrics Charts Section */}
+ {/* Historical Charts Section - Single comprehensive section */}
{server && (
-
+
)}
diff --git a/application/src/services/dockerService.ts b/application/src/services/dockerService.ts
index d01e98e..8047240 100644
--- a/application/src/services/dockerService.ts
+++ b/application/src/services/dockerService.ts
@@ -5,25 +5,25 @@ import { DockerContainer, DockerMetrics, DockerStats } from "@/types/docker.type
class DockerService {
async getContainers(): Promise
{
try {
- console.log('Fetching all Docker containers...');
+ // console.log('Fetching all Docker containers...');
const records = await pb.collection('dockers').getFullList({
sort: '-created',
});
- console.log('Docker containers fetched:', records);
+ // console.log('Docker containers fetched:', records);
return records as DockerContainer[];
} catch (error) {
- console.error('Error fetching Docker containers:', error);
+ // console.error('Error fetching Docker containers:', error);
throw error;
}
}
async getContainersByServerId(serverId: string): Promise {
try {
- console.log('Fetching Docker containers for server ID:', serverId);
+ // console.log('Fetching Docker containers for server ID:', serverId);
// First, try to get the server details to find the correct hostname
const server = await pb.collection('servers').getOne(serverId);
- console.log('Server details:', server);
+ // console.log('Server details:', server);
// Try multiple filter approaches to find containers
const filters = [
@@ -36,57 +36,57 @@ class DockerService {
let containers: DockerContainer[] = [];
for (const filter of filters) {
- console.log('Trying filter:', filter);
+ // console.log('Trying filter:', filter);
try {
const records = await pb.collection('dockers').getFullList({
filter: filter,
sort: '-created',
});
- console.log(`Filter "${filter}" returned:`, records);
+ // console.log(`Filter "${filter}" returned:`, records);
if (records.length > 0) {
containers = records as DockerContainer[];
break;
}
} catch (filterError) {
- console.warn(`Filter "${filter}" failed:`, filterError);
+ // console.warn(`Filter "${filter}" failed:`, filterError);
continue;
}
}
// If no containers found with filters, get all and log for debugging
if (containers.length === 0) {
- console.log('No containers found with filters, fetching all for debugging...');
+ // console.log('No containers found with filters, fetching all for debugging...');
const allContainers = await pb.collection('dockers').getFullList({
sort: '-created',
});
- console.log('All available Docker containers:', allContainers);
- console.log('Looking for containers that might match server:', {
- serverId,
- serverHostname: server.hostname,
- serverIp: server.ip_address
- });
+ // console.log('All available Docker containers:', allContainers);
+ // console.log('Looking for containers that might match server:', {
+ //// serverId,
+ // serverHostname: server.hostname,
+ // serverIp: server.ip_address
+ // });
}
return containers;
} catch (error) {
- console.error('Error fetching Docker containers by server ID:', error);
+ // console.error('Error fetching Docker containers by server ID:', error);
throw error;
}
}
async getContainerMetrics(dockerId: string): Promise {
try {
- console.log('Fetching metrics for docker ID:', dockerId);
+ // console.log('Fetching metrics for docker ID:', dockerId);
const records = await pb.collection('docker_metrics').getFullList({
filter: `docker_id = "${dockerId}"`,
sort: '-timestamp',
perPage: 100,
});
- console.log('Docker metrics fetched:', records);
+ // console.log('Docker metrics fetched:', records);
return records as DockerMetrics[];
} catch (error) {
- console.error('Error fetching Docker metrics:', error);
+ // console.error('Error fetching Docker metrics:', error);
throw error;
}
}
diff --git a/application/src/services/monitoring/service-status/pauseMonitoring.ts b/application/src/services/monitoring/service-status/pauseMonitoring.ts
index 677b09a..9fac22e 100644
--- a/application/src/services/monitoring/service-status/pauseMonitoring.ts
+++ b/application/src/services/monitoring/service-status/pauseMonitoring.ts
@@ -13,7 +13,7 @@ export async function pauseMonitoring(serviceId: string): Promise {
if (intervalId) {
clearInterval(intervalId);
monitoringIntervals.delete(serviceId);
- console.log(`Monitoring paused for service ${serviceId}`);
+ // console.log(`Monitoring paused for service ${serviceId}`);
}
// Get current timestamp formatted as a string
diff --git a/application/src/services/monitoring/service-status/startMonitoring.ts b/application/src/services/monitoring/service-status/startMonitoring.ts
index a1d7a11..5cfb9e8 100644
--- a/application/src/services/monitoring/service-status/startMonitoring.ts
+++ b/application/src/services/monitoring/service-status/startMonitoring.ts
@@ -3,13 +3,13 @@ import { pb } from '@/lib/pocketbase';
import { monitoringIntervals } from '../monitoringIntervals';
/**
- * Start monitoring for a specific service
+ * Start monitoring for a specific service with optimized performance
*/
export async function startMonitoringService(serviceId: string): Promise {
try {
// First check if the service is already being monitored
if (monitoringIntervals.has(serviceId)) {
- // console.log(`Service ${serviceId} is already being monitored`);
+ // console.log(`Service ${serviceId} is already being monitored`);
return;
}
@@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise {
return;
}
- // console.log(`Starting monitoring for service ${serviceId} (${service.name})`);
+ // console.log(`Starting optimized monitoring for service ${serviceId} (${service.name})`);
// Update the service status to active/up in the database
await pb.collection('services').update(serviceId, {
@@ -30,20 +30,24 @@ export async function startMonitoringService(serviceId: string): Promise {
});
// The actual service checking is now handled by the Go microservice
- // This frontend service just tracks the monitoring state
- const intervalMs = (service.heartbeat_interval || 60) * 1000;
- // console.log(`Service ${service.name} monitoring delegated to backend service`);
+ // This frontend service just tracks the monitoring state with much less frequency
+ const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute
+ // console.log(`Service ${service.name} monitoring delegated to backend service with interval ${intervalMs}ms`);
// Store a placeholder interval to track that this service is being monitored
+ // Significantly reduce frequency to prevent excessive logging and CPU usage
const intervalId = window.setInterval(() => {
- // console.log(`Monitoring active for service ${service.name} (handled by backend)`);
- }, intervalMs);
+ // Only log every 5 minutes to reduce console spam
+ if (Date.now() % (5 * 60 * 1000) < intervalMs) {
+ // console.log(`Monitoring active for service ${service.name} (handled by backend)`);
+ }
+ }, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging
// Store the interval ID for this service
monitoringIntervals.set(serviceId, intervalId);
- // console.log(`Monitoring registered for service ${serviceId}`);
+ // console.log(`Optimized monitoring registered for service ${serviceId}`);
} catch (error) {
- // console.error("Error starting service monitoring:", error);
+ // console.error("Error starting service monitoring:", error);
}
}
\ No newline at end of file
diff --git a/application/src/services/operationalPageService.ts b/application/src/services/operationalPageService.ts
index 28d905b..0417755 100644
--- a/application/src/services/operationalPageService.ts
+++ b/application/src/services/operationalPageService.ts
@@ -27,7 +27,7 @@ export const operationalPageService = {
const result = await response.json();
return result.items || [];
} catch (error) {
- console.error('Error fetching operational pages:', error);
+ // console.error('Error fetching operational pages:', error);
throw error;
}
},
@@ -55,7 +55,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
- console.error('Error fetching operational page:', error);
+ // console.error('Error fetching operational page:', error);
throw error;
}
},
@@ -84,7 +84,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
- console.error('Error updating operational page:', error);
+ // console.error('Error updating operational page:', error);
throw error;
}
},
@@ -113,7 +113,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
- console.error('Error creating operational page:', error);
+ // console.error('Error creating operational page:', error);
throw error;
}
},
@@ -139,7 +139,7 @@ export const operationalPageService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
- console.error('Error deleting operational page:', error);
+ // console.error('Error deleting operational page:', error);
throw error;
}
}
diff --git a/application/src/services/serverService.ts b/application/src/services/serverService.ts
index c808783..1bdda23 100644
--- a/application/src/services/serverService.ts
+++ b/application/src/services/serverService.ts
@@ -24,183 +24,144 @@ export const serverService = {
async getServerMetrics(serverId: string, timeRange?: string): Promise {
try {
- // console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange);
+ // console.log('π serverService.getServerMetrics: Starting with serverId:', serverId, 'timeRange:', timeRange);
// First, get the server to find the correct server_id for metrics
let server;
try {
server = await this.getServer(serverId);
- // console.log('serverService.getServerMetrics: Found server:', server);
+ // console.log('β
serverService.getServerMetrics: Found server:', {
+ // id: server.id,
+ // server_id: server.server_id,
+ // name: server.name
+ // });
} catch (error) {
- // console.log('serverService.getServerMetrics: Could not fetch server details:', error);
+ // console.log('β serverService.getServerMetrics: Could not fetch server details:', error);
}
- // Try multiple filter strategies to find data
- let filter = '';
- let metricsServerId = serverId;
-
- // Strategy 1: Use server.server_id if available
- if (server && server.server_id) {
- metricsServerId = server.server_id;
- filter = `server_id = "${metricsServerId}"`;
- // console.log('serverService.getServerMetrics: Strategy 1 - Using server.server_id for metrics:', metricsServerId);
- } else {
- // Strategy 2: Use the serverId directly
- filter = `server_id = "${serverId}"`;
- // console.log('serverService.getServerMetrics: Strategy 2 - Using serverId directly for metrics:', serverId);
- }
-
- // Add agent_id filter if available in server data
- if (server && server.agent_id) {
- filter += ` && agent_id = "${server.agent_id}"`;
- // console.log('serverService.getServerMetrics: Added agent_id filter:', server.agent_id);
- }
-
- // Add time range filter
- if (timeRange) {
- const now = new Date();
- let cutoffTime;
-
- switch (timeRange) {
- case '60m':
- cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
- break;
- case '1d':
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- break;
- case '7d':
- cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
- break;
- case '1m':
- cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
- break;
- case '3m':
- cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
- break;
- default:
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- }
-
- const cutoffISO = cutoffTime.toISOString();
- filter += ` && created >= "${cutoffISO}"`;
- // console.log('serverService.getServerMetrics: Using time filter from:', cutoffISO, 'to now');
- }
-
- // console.log('serverService.getServerMetrics: Final filter:', filter);
-
- // Fetch filtered records with proper sorting
- let records = await pb.collection('server_metrics').getFullList({
- filter: filter,
+ // Let's first check what data exists in the database for this server
+ // console.log('π Checking all records for this server...');
+ const allServerRecords = await pb.collection('server_metrics').getFullList({
+ filter: `server_id = "${serverId}" || server_id = "${server?.server_id}" || server_id = "${server?.id}"`,
sort: '-created',
requestKey: null
});
-
- // console.log('serverService.getServerMetrics: Found', records.length, 'records with primary filter');
- // If no records found with primary strategy, try fallback strategies
- if (records.length === 0) {
- // console.log('serverService.getServerMetrics: No records found, trying fallback strategies...');
+ // console.log('π Found total records for server:', allServerRecords.length);
+ if (allServerRecords.length > 0) {
+ // console.log('π
Date range of all records:', {
+ // newest: allServerRecords[0]?.created,
+ // oldest: allServerRecords[allServerRecords.length - 1]?.created
+ // });
- // Fallback 1: Try without agent_id filter
- let fallbackFilter = `server_id = "${metricsServerId}"`;
- if (timeRange) {
- const now = new Date();
- let cutoffTime;
-
- switch (timeRange) {
- case '60m':
- cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
- break;
- case '1d':
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- break;
- case '7d':
- cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
- break;
- case '1m':
- cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
- break;
- case '3m':
- cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
- break;
- default:
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- }
-
+ // Check last 5 records
+ // console.log('π Last 5 records timestamps:', allServerRecords.slice(0, 5).map(r => ({
+ // created: r.created,
+ // age_minutes: Math.round((new Date().getTime() - new Date(r.created).getTime()) / (1000 * 60))
+ // })));
+ }
+
+ // Calculate time range for filtering
+ const now = new Date();
+ let cutoffTime;
+
+ if (timeRange === '60m') {
+ cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); // Exactly 60 minutes
+ // console.log('β° 60m filter: Looking for records newer than:', cutoffTime.toISOString());
+ // console.log('β° Current time:', now.toISOString());
+ // console.log('β° Time difference in minutes:', Math.round((now.getTime() - cutoffTime.getTime()) / (1000 * 60)));
+ } else if (timeRange === '1d') {
+ cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
+ } else if (timeRange === '7d') {
+ cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
+ } else if (timeRange === '1m') {
+ cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
+ } else if (timeRange === '3m') {
+ cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
+ } else {
+ cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
+ }
+
+ // Try to get filtered records
+ const searchStrategies = [
+ server?.server_id,
+ serverId,
+ server?.id
+ ].filter(Boolean);
+
+ let filteredRecords: any[] = [];
+
+ for (const strategy of searchStrategies) {
+ try {
const cutoffISO = cutoffTime.toISOString();
- fallbackFilter += ` && created >= "${cutoffISO}"`;
- }
-
- // console.log('serverService.getServerMetrics: Trying fallback filter without agent_id:', fallbackFilter);
- records = await pb.collection('server_metrics').getFullList({
- filter: fallbackFilter,
- sort: '-created',
- requestKey: null
- });
-
- // console.log('serverService.getServerMetrics: Fallback found', records.length, 'records');
-
- // Fallback 2: Try with different server_id strategies
- if (records.length === 0) {
- const alternativeIds = [serverId, server?.server_id, server?.id].filter(Boolean);
- // console.log('serverService.getServerMetrics: Trying alternative server IDs:', alternativeIds);
+ const filter = `server_id = "${strategy}" && created >= "${cutoffISO}"`;
- for (const altId of alternativeIds) {
- if (altId && altId !== metricsServerId) {
- let altFilter = `server_id = "${altId}"`;
- if (timeRange) {
- const now = new Date();
- let cutoffTime;
-
- switch (timeRange) {
- case '60m':
- cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
- break;
- case '1d':
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- break;
- case '7d':
- cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
- break;
- case '1m':
- cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
- break;
- case '3m':
- cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
- break;
- default:
- cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
- }
-
- const cutoffISO = cutoffTime.toISOString();
- altFilter += ` && created >= "${cutoffISO}"`;
- }
+ // console.log(`π Trying filter: ${filter}`);
+
+ const records = await pb.collection('server_metrics').getFullList({
+ filter: filter,
+ sort: '-created',
+ requestKey: null
+ });
+
+ // console.log(`π Strategy "${strategy}" found ${records.length} records within time range`);
+
+ if (records.length > 0) {
+ filteredRecords = records;
+ // console.log('β
Using records from strategy:', strategy);
+ break;
+ }
+ } catch (error) {
+ // console.error(`β Error with strategy ${strategy}:`, error);
+ continue;
+ }
+ }
+
+ // If no filtered records found and it's 60m, let's see what we have in a larger window
+ if (filteredRecords.length === 0 && timeRange === '60m') {
+ // console.log('β οΈ No records found in 60m window, checking last 24 hours...');
+
+ const last24h = new Date(now.getTime() - (24 * 60 * 60 * 1000));
+
+ for (const strategy of searchStrategies) {
+ try {
+ const filter = `server_id = "${strategy}" && created >= "${last24h.toISOString()}"`;
+
+ const records = await pb.collection('server_metrics').getFullList({
+ filter: filter,
+ sort: '-created',
+ requestKey: null
+ });
+
+ // console.log(`π Last 24h check for "${strategy}": ${records.length} records`);
+
+ if (records.length > 0) {
+ // console.log('π
Sample record ages (minutes ago):', records.slice(0, 3).map(r =>
+ // Math.round((now.getTime() - new Date(r.created).getTime()) / (1000 * 60))
+ // ));
- // console.log('serverService.getServerMetrics: Trying alternative ID filter:', altFilter);
- const altRecords = await pb.collection('server_metrics').getFullList({
- filter: altFilter,
- sort: '-created',
- requestKey: null
- });
-
- if (altRecords.length > 0) {
- // console.log('serverService.getServerMetrics: Alternative ID found', altRecords.length, 'records');
- records = altRecords;
- break;
- }
+ // Return all recent records for 60m if we have any
+ filteredRecords = records;
+ break;
}
+ } catch (error) {
+ // console.error(`β Error with 24h fallback for ${strategy}:`, error);
+ continue;
}
}
}
- // console.log('serverService.getServerMetrics: Final result:', records.length, 'records found');
- if (records.length > 0) {
- // console.log('serverService.getServerMetrics: Sample record:', records[0]);
+ // console.log('π― Final result:', filteredRecords.length, 'records found for', timeRange);
+ if (filteredRecords.length > 0) {
+ // console.log('π
Returned records age range (minutes ago):', {
+ // newest: Math.round((now.getTime() - new Date(filteredRecords[0].created).getTime()) / (1000 * 60)),
+ // oldest: Math.round((now.getTime() - new Date(filteredRecords[filteredRecords.length - 1].created).getTime()) / (1000 * 60))
+ // });
}
- return records;
+ return filteredRecords;
} catch (error) {
- // console.error('Error fetching server metrics:', error);
+ // console.error('β Error fetching server metrics:', error);
throw error;
}
},
diff --git a/application/src/services/serverThresholdService.ts b/application/src/services/serverThresholdService.ts
new file mode 100644
index 0000000..4a2804c
--- /dev/null
+++ b/application/src/services/serverThresholdService.ts
@@ -0,0 +1,85 @@
+
+import { pb } from "@/lib/pocketbase";
+
+export interface ServerThreshold {
+ id: string;
+ name: string;
+ cpu_threshold: number;
+ ram_threshold: number;
+ disk_threshold: number;
+ network_threshold: number;
+ created: string;
+ updated: string;
+}
+
+export interface CreateUpdateServerThresholdData {
+ name: string;
+ cpu_threshold: number;
+ ram_threshold: number;
+ disk_threshold: number;
+ network_threshold: number;
+}
+
+export const serverThresholdService = {
+ async getServerThresholds(): Promise {
+ try {
+ // console.log("Fetching server threshold templates");
+ const response = await pb.collection('server_threshold_templates').getList(1, 50, {
+ sort: '-created',
+ });
+ // console.log("Server threshold templates response:", response);
+ return response.items as unknown as ServerThreshold[];
+ } catch (error) {
+ // console.error("Error fetching server threshold templates:", error);
+ throw error;
+ }
+ },
+
+ async getServerThreshold(id: string): Promise {
+ try {
+ // console.log(`Fetching server threshold template with id: ${id}`);
+ const response = await pb.collection('server_threshold_templates').getOne(id);
+ // console.log("Server threshold template response:", response);
+ return response as unknown as ServerThreshold;
+ } catch (error) {
+ // console.error(`Error fetching server threshold template with id ${id}:`, error);
+ throw error;
+ }
+ },
+
+ async createServerThreshold(data: CreateUpdateServerThresholdData): Promise {
+ try {
+ // console.log("Creating new server threshold template with data:", data);
+ const response = await pb.collection('server_threshold_templates').create(data);
+ // console.log("Create server threshold template response:", response);
+ return response as unknown as ServerThreshold;
+ } catch (error) {
+ // console.error("Error creating server threshold template:", error);
+ throw error;
+ }
+ },
+
+ async updateServerThreshold(id: string, data: Partial): Promise {
+ try {
+ // console.log(`Updating server threshold template with id: ${id}`, data);
+ const response = await pb.collection('server_threshold_templates').update(id, data);
+ // console.log("Update server threshold template response:", response);
+ return response as unknown as ServerThreshold;
+ } catch (error) {
+ // console.error(`Error updating server threshold template with id ${id}:`, error);
+ throw error;
+ }
+ },
+
+ async deleteServerThreshold(id: string): Promise {
+ try {
+ // console.log(`Deleting server threshold template with id: ${id}`);
+ await pb.collection('server_threshold_templates').delete(id);
+ // console.log("Server threshold template deleted successfully");
+ return true;
+ } catch (error) {
+ // console.error(`Error deleting server threshold template with id ${id}:`, error);
+ throw error;
+ }
+ }
+};
\ No newline at end of file
diff --git a/application/src/services/ssl/notification/sslCheckNotifier.ts b/application/src/services/ssl/notification/sslCheckNotifier.ts
index 5657118..e35a494 100644
--- a/application/src/services/ssl/notification/sslCheckNotifier.ts
+++ b/application/src/services/ssl/notification/sslCheckNotifier.ts
@@ -1,120 +1,53 @@
import { pb } from "@/lib/pocketbase";
-import { SSLCertificate } from "../types";
-import { determineSSLStatus } from "../sslStatusUtils";
-import { sendSSLNotification } from "./sslNotificationSender";
-import { toast } from "sonner";
+import { SSLCertificate } from "@/types/ssl.types";
/**
- * Checks all SSL certificates and sends notifications for expiring ones
- * This should be called once per day
+ * Check a single SSL certificate - notifications are now handled by the backend
*/
-export async function checkAllCertificatesAndNotify(): Promise {
- // console.log("Starting daily SSL certificates check...");
-
+export const checkCertificateAndNotify = async (certificate: SSLCertificate): Promise => {
try {
- // Fetch all SSL certificates from database
- const response = await pb.collection('ssl_certificates').getList(1, 100, {});
- // Properly cast the items as SSLCertificate
- const certificates = response.items as unknown as SSLCertificate[];
+ // console.log(`Checking certificate for ${certificate.domain}...`);
- // console.log(`Found ${certificates.length} certificates to check`);
+ // The actual SSL checking and notifications are now handled by the Go service
+ // We just need to trigger a check by updating the check_at timestamp
- // Check each certificate
- for (const cert of certificates) {
- await checkCertificateAndNotify(cert);
- }
+ const now = new Date();
- // console.log("Daily SSL certificates check completed");
- } catch (error) {
- // console.error("Error during SSL certificates daily check:", error);
- }
-}
-
-/**
- * Checks a specific SSL certificate and sends notification if needed
- * This respects the Warning and Expiry Thresholds set on the certificate
- * Note: SSL checking is now handled by the Go service, this function focuses on notifications
- */
-export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise {
-// console.log(`Checking certificate for ${certificate.domain}...`);
-
- try {
- // Use the current certificate data (updated by Go service)
- const daysLeft = certificate.days_left || 0;
-
- // Get threshold values (ensure they are numbers)
- const warningThreshold = Number(certificate.warning_threshold) || 30;
- const expiryThreshold = Number(certificate.expiry_threshold) || 7;
-
- // console.log(`Certificate ${certificate.domain} thresholds: warning=${warningThreshold}, expiry=${expiryThreshold}, days left=${daysLeft}`);
-
- // Update status based on thresholds
- const status = determineSSLStatus(daysLeft, warningThreshold, expiryThreshold);
-
- // Check if we should send a notification based on thresholds
- let shouldNotify = false;
- let isCritical = false;
-
- // Critical notifications - when days left is less than or equal to expiry threshold
- if (daysLeft <= expiryThreshold) {
- shouldNotify = true;
- isCritical = true;
- }
- // Warning notifications - when days left is less than or equal to warning threshold but greater than expiry threshold
- else if (daysLeft <= warningThreshold) {
- shouldNotify = true;
- isCritical = false;
- }
-
- // console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`);
-
- // Update certificate status in database
+ // Update check_at to trigger backend check
await pb.collection('ssl_certificates').update(certificate.id, {
- status: status
+ check_at: now.toISOString()
});
-
- // Send notification if needed
- if (shouldNotify && certificate.notification_channel) {
- // console.log(`Sending notification for ${certificate.domain}`);
-
- // Different message based on expiry threshold
- const message = isCritical
- ? `π¨ CRITICAL: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days!`
- : `β οΈ WARNING: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days.`;
-
- // Send the notification using our specialized SSL notification sender
- const notificationSent = await sendSSLNotification(certificate, message, isCritical);
-
- if (notificationSent) {
- // Update last_notified timestamp
- await pb.collection('ssl_certificates').update(certificate.id, {
- last_notified: new Date().toISOString()
- });
- // console.log(`Notification sent for ${certificate.domain}`);
- // Show toast for manual checks
- toast.success(`Notification sent for ${certificate.domain}`);
- return true;
- } else {
- // console.error(`Failed to send notification for ${certificate.domain}`);
- // Show error toast for manual checks
- toast.error(`Failed to send notification for ${certificate.domain}`);
- return false;
+ } catch (error) {
+ throw error;
+ }
+};
+
+/**
+ * Check all SSL certificates - backend handles the actual checking and notifications
+ */
+export const checkAllCertificatesAndNotify = async (): Promise<{ success: number; failed: number }> => {
+ try {
+
+ const response = await pb.collection('ssl_certificates').getList(1, 100);
+ const certificates = response.items as unknown as SSLCertificate[];
+
+ let success = 0;
+ let failed = 0;
+
+ for (const cert of certificates) {
+ try {
+ await checkCertificateAndNotify(cert);
+ success++;
+ } catch (error) {
+ failed++;
}
- } else if (shouldNotify && !certificate.notification_channel) {
- // console.log(`No notification channel set for ${certificate.domain}, skipping notification`);
- toast.info(`No notification channel set for ${certificate.domain}, skipping notification`);
- } else {
- // console.log(`No notification needed for ${certificate.domain} (${daysLeft} days left)`);
- // For manual checks, inform the user that thresholds weren't met
- toast.info(`Certificate for ${certificate.domain} is valid (${daysLeft} days left)`);
}
- return true;
+ return { success, failed };
+
} catch (error) {
- // console.error(`Error checking certificate for ${certificate.domain}:`, error);
- toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`);
- return false;
+ throw error;
}
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/application/src/services/statusPageComponentsService.ts b/application/src/services/statusPageComponentsService.ts
index d55f334..6e91d10 100644
--- a/application/src/services/statusPageComponentsService.ts
+++ b/application/src/services/statusPageComponentsService.ts
@@ -27,7 +27,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
- console.error('Error fetching status page components:', error);
+ // console.error('Error fetching status page components:', error);
throw error;
}
},
@@ -56,7 +56,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
- console.error('Error fetching status page components by operational ID:', error);
+ // console.error('Error fetching status page components by operational ID:', error);
throw error;
}
},
@@ -79,7 +79,7 @@ export const statusPageComponentsService = {
headers['Authorization'] = `Bearer ${authToken}`;
}
- console.log('Creating status page component with data:', data);
+ // console.log('Creating status page component with data:', data);
const baseUrl = getCurrentEndpoint();
const response = await fetch(`${baseUrl}/api/collections/status_page_components/records`, {
@@ -95,10 +95,10 @@ export const statusPageComponentsService = {
}
const result = await response.json();
- console.log('Created component:', result);
+ // console.log('Created component:', result);
return result;
} catch (error) {
- console.error('Error creating status page component:', error);
+ // console.error('Error creating status page component:', error);
throw error;
}
},
@@ -124,7 +124,7 @@ export const statusPageComponentsService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
- console.error('Error deleting status page component:', error);
+ // console.error('Error deleting status page component:', error);
throw error;
}
}
diff --git a/application/src/services/templateService.ts b/application/src/services/templateService.ts
index 85d5536..f1c37fe 100644
--- a/application/src/services/templateService.ts
+++ b/application/src/services/templateService.ts
@@ -1,4 +1,5 @@
+
import { pb } from "@/lib/pocketbase";
export interface NotificationTemplate {
@@ -37,62 +38,62 @@ export interface CreateUpdateTemplateData {
export const templateService = {
async getTemplates(): Promise {
try {
- console.log("Fetching notification templates");
- const response = await pb.collection('notification_templates').getList(1, 50, {
+ // console.log("Fetching server notification templates");
+ const response = await pb.collection('server_notification_templates').getList(1, 50, {
sort: '-created',
});
- console.log("Templates response:", response);
+ // console.log("Server templates response:", response);
return response.items as unknown as NotificationTemplate[];
} catch (error) {
- console.error("Error fetching templates:", error);
+ // console.error("Error fetching server templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise {
try {
- console.log(`Fetching template with id: ${id}`);
- const response = await pb.collection('notification_templates').getOne(id);
- console.log("Template response:", response);
+ // console.log(`Fetching server template with id: ${id}`);
+ const response = await pb.collection('server_notification_templates').getOne(id);
+ // console.log("Server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
- console.error(`Error fetching template with id ${id}:`, error);
+ // console.error(`Error fetching server template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateTemplateData): Promise {
try {
- console.log("Creating new template with data:", data);
- const response = await pb.collection('notification_templates').create(data);
- console.log("Create template response:", response);
+ // console.log("Creating new server template with data:", data);
+ const response = await pb.collection('server_notification_templates').create(data);
+ // console.log("Create server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
- console.error("Error creating template:", error);
+ // console.error("Error creating server template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial): Promise {
try {
- console.log(`Updating template with id: ${id}`, data);
- const response = await pb.collection('notification_templates').update(id, data);
- console.log("Update template response:", response);
+ // console.log(`Updating server template with id: ${id}`, data);
+ const response = await pb.collection('server_notification_templates').update(id, data);
+ // console.log("Update server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
- console.error(`Error updating template with id ${id}:`, error);
+ // console.error(`Error updating server template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise {
try {
- console.log(`Deleting template with id: ${id}`);
- await pb.collection('notification_templates').delete(id);
- console.log("Template deleted successfully");
+ // console.log(`Deleting server template with id: ${id}`);
+ await pb.collection('server_notification_templates').delete(id);
+ // console.log("Server template deleted successfully");
return true;
} catch (error) {
- console.error(`Error deleting template with id ${id}:`, error);
+ // console.error(`Error deleting server template with id ${id}:`, error);
throw error;
}
}
diff --git a/application/src/types/server.types.ts b/application/src/types/server.types.ts
index de0c42d..90e0f2b 100644
--- a/application/src/types/server.types.ts
+++ b/application/src/types/server.types.ts
@@ -8,7 +8,7 @@ export interface Server {
hostname: string;
ip_address: string;
os_type: string;
- status: 'up' | 'down' | 'warning';
+ status: 'up' | 'down' | 'warning' | 'paused';
uptime: string;
ram_total: number;
ram_used: number;
@@ -19,6 +19,7 @@ export interface Server {
last_checked: string;
server_token: string;
template_id: string;
+ threshold_id: string;
notification_id: string;
timestamp: string;
connection: string;
diff --git a/application/src/utils/copyUtils.ts b/application/src/utils/copyUtils.ts
new file mode 100644
index 0000000..24483d4
--- /dev/null
+++ b/application/src/utils/copyUtils.ts
@@ -0,0 +1,60 @@
+
+import { toast } from "@/hooks/use-toast";
+
+export const copyToClipboard = async (text: string) => {
+ console.log('copyToClipboard called with text:', text); // Debug log
+
+ try {
+ // Try modern clipboard API first
+ if (navigator.clipboard && window.isSecureContext) {
+ console.log('Using modern clipboard API'); // Debug log
+ await navigator.clipboard.writeText(text);
+ toast({
+ title: "Copied!",
+ description: "Content copied to clipboard successfully.",
+ });
+ return;
+ }
+
+ console.log('Using fallback clipboard method'); // Debug log
+
+ // Fallback for older browsers or non-secure contexts
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ textArea.style.position = "fixed";
+ textArea.style.left = "-9999px";
+ textArea.style.top = "-9999px";
+ textArea.style.opacity = "0";
+ textArea.style.pointerEvents = "none";
+ textArea.style.zIndex = "-1";
+ document.body.appendChild(textArea);
+
+ // Focus and select the text
+ textArea.focus();
+ textArea.select();
+ textArea.setSelectionRange(0, textArea.value.length);
+
+ // Use execCommand as fallback
+ const successful = document.execCommand('copy');
+ document.body.removeChild(textArea);
+
+ if (successful) {
+ console.log('Copy successful with execCommand'); // Debug log
+ toast({
+ title: "Copied!",
+ description: "Content copied to clipboard successfully.",
+ });
+ } else {
+ throw new Error('Copy command failed');
+ }
+ } catch (error) {
+ console.error('Failed to copy to clipboard:', error);
+
+ // Show error toast
+ toast({
+ title: "Copy Failed",
+ description: "Unable to copy automatically. Please select and copy the text manually.",
+ variant: "destructive",
+ });
+ }
+};
\ No newline at end of file
diff --git a/scripts/server-agent.sh b/scripts/server-agent.sh
new file mode 100644
index 0000000..e73db52
--- /dev/null
+++ b/scripts/server-agent.sh
@@ -0,0 +1,460 @@
+
+#!/bin/bash
+
+# CheckCle Server Monitoring Agent - One-Click Installation Script
+# This script provides fully automated installation using environment variables
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CONFIG_FILE="/etc/monitoring-agent/monitoring-agent.env"
+
+# GitHub release base URL
+GITHUB_BASE_URL="https://github.com/operacle/checke-server-agent/releases/download/v1.0.0"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Helper functions
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Check if running as root
+check_root() {
+ if [[ $EUID -ne 0 ]]; then
+ log_error "This script must be run as root (use sudo)"
+ exit 1
+ fi
+}
+
+# Detect system architecture and package format
+detect_system() {
+ log_info "Detecting system architecture and package format..."
+
+ # Detect architecture
+ ARCH=$(uname -m)
+ case $ARCH in
+ x86_64)
+ PACKAGE_ARCH="amd64"
+ ;;
+ aarch64|arm64)
+ PACKAGE_ARCH="arm64"
+ ;;
+ *)
+ log_error "Unsupported architecture: $ARCH"
+ log_info "Supported architectures: x86_64 (amd64), aarch64/arm64"
+ exit 1
+ ;;
+ esac
+
+ # Detect package format preference
+ if command -v dpkg >/dev/null 2>&1; then
+ PACKAGE_FORMAT="deb"
+ PACKAGE_MANAGER="dpkg"
+ elif command -v rpm >/dev/null 2>&1; then
+ PACKAGE_FORMAT="rpm"
+ if command -v yum >/dev/null 2>&1; then
+ PACKAGE_MANAGER="yum"
+ elif command -v dnf >/dev/null 2>&1; then
+ PACKAGE_MANAGER="dnf"
+ else
+ PACKAGE_MANAGER="rpm"
+ fi
+ else
+ log_error "No supported package manager found (dpkg or rpm required)"
+ log_info "This script supports Debian/Ubuntu (.deb) and RHEL/CentOS/Fedora (.rpm) systems"
+ exit 1
+ fi
+
+ # Construct package filename and URL
+ PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}"
+ PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}"
+
+ log_success "System detection complete:"
+ log_info " Architecture: $ARCH -> $PACKAGE_ARCH"
+ log_info " Package format: $PACKAGE_FORMAT"
+ log_info " Package manager: $PACKAGE_MANAGER"
+ log_info " Package URL: $PACKAGE_URL"
+}
+
+# Validate required environment variables - check both current and sudo environments
+validate_environment() {
+ # Check if SERVER_TOKEN is available in current environment or passed as argument
+ if [[ -z "$SERVER_TOKEN" ]]; then
+ log_error "SERVER_TOKEN environment variable is required"
+ log_info "Usage examples:"
+ log_info " SERVER_TOKEN=your-token sudo -E bash $0"
+ log_info " sudo SERVER_TOKEN=your-token bash $0"
+ log_info " curl -L script-url | SERVER_TOKEN=your-token sudo bash"
+ log_info ""
+ log_info "Required: SERVER_TOKEN"
+ log_info "Optional: POCKETBASE_URL, SERVER_NAME, AGENT_ID, HEALTH_CHECK_PORT"
+ exit 1
+ fi
+
+ log_success "Environment validation passed"
+ log_info "SERVER_TOKEN: ${SERVER_TOKEN:0:8}..."
+ [[ -n "$POCKETBASE_URL" ]] && log_info "POCKETBASE_URL: $POCKETBASE_URL"
+ [[ -n "$SERVER_NAME" ]] && log_info "SERVER_NAME: $SERVER_NAME"
+ [[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID"
+}
+
+# Download package based on detected system
+download_package() {
+ local temp_dir="/tmp/monitoring-agent-install"
+ mkdir -p "$temp_dir"
+
+ log_info "Downloading monitoring agent package..."
+ log_info "URL: $PACKAGE_URL"
+
+ if curl -L -f -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL"; then
+ DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
+ log_success "Package downloaded successfully: $PACKAGE_FILENAME"
+ else
+ log_error "Failed to download package from: $PACKAGE_URL"
+ log_info "Please check:"
+ log_info " 1. Internet connectivity"
+ log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)"
+ log_info " 3. GitHub repository access"
+ exit 1
+ fi
+}
+
+# Install package based on detected package manager
+install_package() {
+ log_info "Installing monitoring agent package using $PACKAGE_MANAGER..."
+
+ case $PACKAGE_MANAGER in
+ dpkg)
+ # Update package lists
+ apt-get update -qq
+
+ # Install the package
+ if dpkg -i "$DOWNLOADED_PACKAGE" 2>/dev/null; then
+ log_success "DEB package installed successfully"
+ else
+ log_warning "Package installation had dependency issues, fixing..."
+ apt-get install -f -y
+ log_success "Dependencies resolved and package installed"
+ fi
+ ;;
+
+ rpm)
+ # Install the package directly
+ if rpm -ivh "$DOWNLOADED_PACKAGE" 2>/dev/null; then
+ log_success "RPM package installed successfully"
+ else
+ log_error "RPM package installation failed"
+ log_info "Try installing manually: sudo rpm -ivh $DOWNLOADED_PACKAGE"
+ exit 1
+ fi
+ ;;
+
+ yum)
+ if yum localinstall -y "$DOWNLOADED_PACKAGE"; then
+ log_success "Package installed successfully via YUM"
+ else
+ log_error "YUM package installation failed"
+ exit 1
+ fi
+ ;;
+
+ dnf)
+ if dnf localinstall -y "$DOWNLOADED_PACKAGE"; then
+ log_success "Package installed successfully via DNF"
+ else
+ log_error "DNF package installation failed"
+ exit 1
+ fi
+ ;;
+
+ *)
+ log_error "Unsupported package manager: $PACKAGE_MANAGER"
+ exit 1
+ ;;
+ esac
+}
+
+# Auto-detect system information
+detect_system_info() {
+ log_info "Auto-detecting system information..."
+
+ # Detect hostname
+ DETECTED_HOSTNAME=$(hostname)
+ log_info "Detected hostname: $DETECTED_HOSTNAME"
+
+ # Detect IP address
+ DETECTED_IP=$(ip route get 8.8.8.8 2>/dev/null | head -1 | awk '{print $7}' | head -1)
+ if [[ -z "$DETECTED_IP" ]]; then
+ DETECTED_IP=$(hostname -I | awk '{print $1}')
+ fi
+ log_info "Detected IP address: $DETECTED_IP"
+
+ # Detect OS
+ if [[ -f /etc/os-release ]]; then
+ DETECTED_OS=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"')
+ else
+ DETECTED_OS="linux"
+ fi
+ log_info "Detected OS: $DETECTED_OS"
+}
+
+# Configure agent using environment variables
+configure_agent() {
+ log_info "Configuring agent with provided settings..."
+
+ # Use environment variables or auto-detected defaults
+ SERVER_NAME="${SERVER_NAME:-$DETECTED_HOSTNAME}"
+ POCKETBASE_URL="${POCKETBASE_URL:-http://localhost:8090}"
+ IP_ADDRESS="${IP_ADDRESS:-$DETECTED_IP}"
+ HOSTNAME="${HOSTNAME:-$DETECTED_HOSTNAME}"
+ OS_TYPE="${OS_TYPE:-$DETECTED_OS}"
+ AGENT_ID="${AGENT_ID:-monitoring-agent-$(hostname -s)}"
+ HEALTH_CHECK_PORT="${HEALTH_CHECK_PORT:-8081}"
+
+ log_info "Final configuration:"
+ log_info " Server Name: $SERVER_NAME"
+ log_info " Agent ID: $AGENT_ID"
+ log_info " PB API URL: $POCKETBASE_URL"
+ log_info " IP Address: $IP_ADDRESS"
+ log_info " OS Type: $OS_TYPE"
+ log_info " Health Check Port: $HEALTH_CHECK_PORT"
+
+ write_config
+}
+
+# Write configuration to file
+write_config() {
+ log_info "Writing configuration to $CONFIG_FILE..."
+
+ # Create directory if it doesn't exist
+ mkdir -p "$(dirname "$CONFIG_FILE")"
+
+ cat > "$CONFIG_FILE" << EOF
+# CheckCle Server Monitoring Agent Configuration
+# Generated on $(date)
+
+# Basic Configuration
+AGENT_ID=$AGENT_ID
+CHECK_INTERVAL=30s
+HEALTH_CHECK_PORT=$HEALTH_CHECK_PORT
+
+# PocketBase Configuration
+POCKETBASE_ENABLED=true
+POCKETBASE_URL=$POCKETBASE_URL
+
+# Server Configuration
+SERVER_NAME=$SERVER_NAME
+HOSTNAME=$HOSTNAME
+IP_ADDRESS=$IP_ADDRESS
+OS_TYPE=$OS_TYPE
+SERVER_TOKEN=$SERVER_TOKEN
+
+# Remote Control
+REMOTE_CONTROL_ENABLED=true
+COMMAND_CHECK_INTERVAL=10s
+
+# Monitoring Settings
+REPORT_INTERVAL=5m
+MAX_RETRIES=3
+REQUEST_TIMEOUT=10s
+EOF
+
+ # Set proper permissions
+ chown root:monitoring-agent "$CONFIG_FILE" 2>/dev/null || chown root:root "$CONFIG_FILE"
+ chmod 640 "$CONFIG_FILE"
+
+ log_success "Configuration written successfully"
+}
+
+# Start and enable service
+start_service() {
+ log_info "Starting monitoring agent service..."
+
+ # Reload systemd
+ systemctl daemon-reload
+
+ # Enable service
+ systemctl enable monitoring-agent
+ log_success "Service enabled for auto-start"
+
+ # Start service
+ if systemctl start monitoring-agent; then
+ log_success "Service started successfully"
+ else
+ log_error "Failed to start service"
+ log_info "Check logs with: journalctl -u monitoring-agent -f"
+ return 1
+ fi
+
+ # Check service status
+ sleep 2
+ if systemctl is-active --quiet monitoring-agent; then
+ log_success "Service is running"
+ else
+ log_warning "Service may have issues, checking status..."
+ systemctl status monitoring-agent --no-pager
+ return 1
+ fi
+}
+
+# Test installation
+test_installation() {
+ log_info "Testing installation..."
+
+ # Test health endpoint
+ local health_port=${HEALTH_CHECK_PORT:-8081}
+ log_info "Testing health endpoint at http://localhost:$health_port/health"
+
+ # Wait a moment for service to fully start
+ sleep 3
+
+ if curl -s "http://localhost:$health_port/health" > /dev/null; then
+ log_success "Health endpoint is responding"
+ else
+ log_warning "Health endpoint not responding yet (service may still be starting)"
+ fi
+
+ # Show recent logs
+ log_info "Recent service logs:"
+ journalctl -u monitoring-agent --no-pager -n 5
+}
+
+# Show post-installation information
+show_post_install_info() {
+ echo
+ echo "============================================="
+ echo " CheckCle Server Agent Installation Complete!"
+ echo "============================================="
+ echo
+ log_success "CheckCle Monitoring Agent installed and configured successfully"
+ echo
+ echo "System Information:"
+ echo " Architecture: $ARCH ($PACKAGE_ARCH)"
+ echo " Package: $PACKAGE_FILENAME"
+ echo " Package Manager: $PACKAGE_MANAGER"
+ echo
+ echo "Configuration: $CONFIG_FILE"
+ echo "Service status: systemctl status monitoring-agent"
+ echo "Service logs: journalctl -u monitoring-agent -f"
+ echo "Health check: curl http://localhost:${HEALTH_CHECK_PORT:-8081}/health"
+ echo
+ echo "The monitoring agent is now running and will appear in your dashboard."
+ echo
+}
+
+# Clean up temporary files
+cleanup() {
+ if [[ -n "$DOWNLOADED_PACKAGE" && -f "$DOWNLOADED_PACKAGE" ]]; then
+ rm -f "$DOWNLOADED_PACKAGE"
+ rm -rf "$(dirname "$DOWNLOADED_PACKAGE")"
+ fi
+}
+
+# Main installation function
+main() {
+ echo "============================================="
+ echo " CheckCle Server Monitoring Agent"
+ echo " One-Click Installation"
+ echo "============================================="
+ echo
+
+ check_root
+ validate_environment
+ detect_system
+ detect_system_info
+
+ log_info "Starting automated installation..."
+
+ # Set up cleanup trap
+ trap cleanup EXIT
+
+ # Download and install package
+ download_package
+ install_package
+
+ # Configure and start service
+ configure_agent
+
+ if start_service; then
+ test_installation
+ show_post_install_info
+ else
+ log_error "Service failed to start properly"
+ log_info "Check configuration: sudo nano $CONFIG_FILE"
+ log_info "Restart service: sudo systemctl restart monitoring-agent"
+ exit 1
+ fi
+}
+
+# Handle script arguments
+case "${1:-}" in
+ --help|-h)
+ echo "CheckCle Server Monitoring Agent - One-Click Installer"
+ echo
+ echo "Usage: SERVER_TOKEN=your-token [OPTIONS] sudo bash $0"
+ echo " or: sudo SERVER_TOKEN=your-token [OPTIONS] bash $0"
+ echo " or: curl -L script-url | SERVER_TOKEN=your-token [OPTIONS] sudo bash"
+ echo
+ echo "System Requirements:"
+ echo " - Linux with systemd"
+ echo " - Supported architectures: x86_64 (amd64), aarch64/arm64"
+ echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm)"
+ echo
+ echo "Required Environment Variables:"
+ echo " SERVER_TOKEN Server authentication token"
+ echo
+ echo "Optional Environment Variables:"
+ echo " POCKETBASE_URL PocketBase URL (default: http://localhost:8090)"
+ echo " SERVER_NAME Server name (default: hostname)"
+ echo " AGENT_ID Agent identifier (default: monitoring-agent-hostname)"
+ echo " HEALTH_CHECK_PORT Health check port (default: 8081)"
+ echo
+ echo "Examples:"
+ echo " SERVER_TOKEN=abc123 sudo -E bash $0"
+ echo " sudo SERVER_TOKEN=abc123 POCKETBASE_URL=https://pb.example.com bash $0"
+ echo
+ echo "Options:"
+ echo " --help, -h Show this help message"
+ echo " --uninstall Uninstall the monitoring agent"
+ echo
+ exit 0
+ ;;
+ --uninstall)
+ check_root
+ log_info "Uninstalling monitoring agent..."
+ systemctl stop monitoring-agent 2>/dev/null || true
+ systemctl disable monitoring-agent 2>/dev/null || true
+
+ # Remove based on detected package manager
+ if command -v dpkg >/dev/null 2>&1; then
+ dpkg -r monitoring-agent 2>/dev/null || true
+ elif command -v rpm >/dev/null 2>&1; then
+ rpm -e monitoring-agent 2>/dev/null || true
+ fi
+
+ rm -rf /etc/monitoring-agent 2>/dev/null || true
+ log_success "Monitoring agent uninstalled"
+ exit 0
+ ;;
+ *)
+ main "$@"
+ ;;
+esac
\ No newline at end of file
diff --git a/server/pb_migrations/1752916282_updated_webhook_configs.js b/server/pb_migrations/1752916282_updated_webhook_configs.js
new file mode 100644
index 0000000..79f94a7
--- /dev/null
+++ b/server/pb_migrations/1752916282_updated_webhook_configs.js
@@ -0,0 +1,125 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_45665081")
+
+ // remove field
+ collection.fields.removeById("select4246785570")
+
+ // add field
+ collection.fields.addAt(9, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1579384326",
+ "max": 0,
+ "min": 0,
+ "name": "name",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ // add field
+ collection.fields.addAt(10, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1582905952",
+ "max": 0,
+ "min": 0,
+ "name": "method",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ // add field
+ collection.fields.addAt(11, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1414600993",
+ "max": 0,
+ "min": 0,
+ "name": "payload_template",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ // update field
+ collection.fields.addAt(7, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text2168550802",
+ "max": 0,
+ "min": 0,
+ "name": "trigger_events",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_45665081")
+
+ // add field
+ collection.fields.addAt(4, new Field({
+ "hidden": false,
+ "id": "select4246785570",
+ "maxSelect": 1,
+ "name": "event_filters",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "down",
+ "up",
+ "ssl_expired",
+ "warning",
+ "ssl_ok",
+ "high_cpu",
+ "high_memory",
+ "agent_offline",
+ "custom_alert"
+ ]
+ }))
+
+ // remove field
+ collection.fields.removeById("text1579384326")
+
+ // remove field
+ collection.fields.removeById("text1582905952")
+
+ // remove field
+ collection.fields.removeById("text1414600993")
+
+ // update field
+ collection.fields.addAt(8, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text2168550802",
+ "max": 0,
+ "min": 0,
+ "name": "timeout",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+})
diff --git a/server/pb_migrations/1752921327_updated_alert_configurations.js b/server/pb_migrations/1752921327_updated_alert_configurations.js
new file mode 100644
index 0000000..728ff90
--- /dev/null
+++ b/server/pb_migrations/1752921327_updated_alert_configurations.js
@@ -0,0 +1,42 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1938176441")
+
+ // update field
+ collection.fields.addAt(10, new Field({
+ "hidden": false,
+ "id": "select1358543748",
+ "maxSelect": 1,
+ "name": "status",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "disabled",
+ "enabled"
+ ]
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1938176441")
+
+ // update field
+ collection.fields.addAt(10, new Field({
+ "hidden": false,
+ "id": "select1358543748",
+ "maxSelect": 1,
+ "name": "enabled",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "true",
+ "false"
+ ]
+ }))
+
+ return app.save(collection)
+})
diff --git a/server/pb_migrations/1752921397_updated_alert_configurations.js b/server/pb_migrations/1752921397_updated_alert_configurations.js
new file mode 100644
index 0000000..fad5689
--- /dev/null
+++ b/server/pb_migrations/1752921397_updated_alert_configurations.js
@@ -0,0 +1,29 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1938176441")
+
+ // add field
+ collection.fields.addAt(14, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text1553704459",
+ "max": 0,
+ "min": 0,
+ "name": "webhook_id",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1938176441")
+
+ // remove field
+ collection.fields.removeById("text1553704459")
+
+ return app.save(collection)
+})
diff --git a/server/pb_migrations/1753085447_updated_server_metrics.js b/server/pb_migrations/1753085447_updated_server_metrics.js
new file mode 100644
index 0000000..c0ecf6c
--- /dev/null
+++ b/server/pb_migrations/1753085447_updated_server_metrics.js
@@ -0,0 +1,29 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // add field
+ collection.fields.addAt(19, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3065852031",
+ "max": 0,
+ "min": 0,
+ "name": "message",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // remove field
+ collection.fields.removeById("text3065852031")
+
+ return app.save(collection)
+})
diff --git a/server/pb_migrations/1753102275_updated_server_metrics.js b/server/pb_migrations/1753102275_updated_server_metrics.js
new file mode 100644
index 0000000..acce145
--- /dev/null
+++ b/server/pb_migrations/1753102275_updated_server_metrics.js
@@ -0,0 +1,61 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // add field
+ collection.fields.addAt(20, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text772177811",
+ "max": 0,
+ "min": 0,
+ "name": "detail",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ // update field
+ collection.fields.addAt(19, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3065852031",
+ "max": 0,
+ "min": 0,
+ "name": "error_message",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // remove field
+ collection.fields.removeById("text772177811")
+
+ // update field
+ collection.fields.addAt(19, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3065852031",
+ "max": 0,
+ "min": 0,
+ "name": "message",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+})
diff --git a/server/pb_migrations/1753102296_updated_server_metrics.js b/server/pb_migrations/1753102296_updated_server_metrics.js
new file mode 100644
index 0000000..262830d
--- /dev/null
+++ b/server/pb_migrations/1753102296_updated_server_metrics.js
@@ -0,0 +1,42 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // update field
+ collection.fields.addAt(20, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text772177811",
+ "max": 0,
+ "min": 0,
+ "name": "details",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1998570700")
+
+ // update field
+ collection.fields.addAt(20, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text772177811",
+ "max": 0,
+ "min": 0,
+ "name": "detail",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+})