From 7c08c8bc66b6ed4e8b8288ef93eeb75a69da537b Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 15 Jul 2025 22:12:21 +0700 Subject: [PATCH 01/36] Added CODE OF CONDUCT --- CODE_OF_CONDUCT.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 CODE_OF_CONDUCT.md 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 From 664e2c8ecf610041ef36feefc4f7b3187d85e00c Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 15 Jul 2025 22:13:07 +0700 Subject: [PATCH 02/36] Added ISSUE_TEMPLATE --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/ask_for_help.yml | 53 +++++++++++ .github/ISSUE_TEMPLATE/bug_report.yml | 94 +++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yml | 49 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 63 +++++++++++++ .github/ISSUE_TEMPLATE/security_issue.yml | 51 ++++++++++ .../ISSUE_TEMPLATE/translation_request.yml | 38 ++++++++ 7 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/ask_for_help.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/security_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/translation_request.yml 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/ask_for_help.yml b/.github/ISSUE_TEMPLATE/ask_for_help.yml new file mode 100644 index 0000000..0b058a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask_for_help.yml @@ -0,0 +1,53 @@ +name: Ask for Help +description: Ask a question or request guidance about this project. +title: "[Question]: " +labels: [question] +assignees: [] + +body: + - type: markdown + attributes: + value: | + **Thank you for reaching out!** + Please fill out this form to help us understand your question. + + - 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: Context + description: | + Provide any additional context, related issues, or links that help explain your question. + placeholder: "Related issues, pull requests, or documentation..." + validations: + required: false + + - type: textarea + id: environment + attributes: + label: Environment Details + description: | + If applicable, please provide details about your environment (OS, Node version, browser, etc.). + placeholder: "Example: Ubuntu 22.04, Node.js v20, Chrome 125" + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Before submitting, please confirm: + options: + - label: I have searched existing issues and discussions. + required: true + - label: I have read the documentation. + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..ae7046d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,94 @@ +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 fill out the following template so we can reproduce and fix the issue faster. + + - type: input + id: environment + attributes: + label: Environment + description: | + Describe where you encountered the issue. + (e.g., OS, browser, Node version, etc.) + placeholder: "Example: macOS 14.0, 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 what the bug is. + placeholder: "When I do X, Y happens instead of Z..." + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: | + How can we reproduce the behavior? + Please list the steps in order. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See 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 explain your problem. + placeholder: "Attach screenshots here." + validations: + required: false + + - type: textarea + id: logs + attributes: + label: Logs + description: | + If applicable, paste logs or error messages here. + Please remove any sensitive information. + render: shell + placeholder: | + ``` + Error: Something went wrong + at index.js:123:45 + ``` + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: Checklist + description: Before submitting, please confirm: + options: + - label: I have searched existing issues to avoid duplicates. + required: true + - label: I have provided enough information for reproduction. + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..e4c64a7 --- /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..87cd7ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,63 @@ +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/security_issue.yml b/.github/ISSUE_TEMPLATE/security_issue.yml new file mode 100644 index 0000000..ee44028 --- /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..5e2b226 --- /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 From e4284afb094ea300d3ea101692aa1beb299bc385 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 15 Jul 2025 22:13:46 +0700 Subject: [PATCH 03/36] Remove subtitle under About System page --- application/src/components/settings/about-system/AboutSystem.tsx | 1 - 1 file changed, 1 deletion(-) 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')}

From b4a12a46df39d3001eccd8f4625583a22f5d6c2e Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 15 Jul 2025 22:27:20 +0700 Subject: [PATCH 04/36] Added config file blank issues --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file From 2de3a179676bb4ad55ded1a2d787f274f5bef069 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 15 Jul 2025 22:32:02 +0700 Subject: [PATCH 05/36] Added issue template icon --- .github/ISSUE_TEMPLATE/ask_for_help.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/documentation.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/security_issue.yml | 2 +- .github/ISSUE_TEMPLATE/translation_request.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ask_for_help.yml b/.github/ISSUE_TEMPLATE/ask_for_help.yml index 0b058a2..54e72f7 100644 --- a/.github/ISSUE_TEMPLATE/ask_for_help.yml +++ b/.github/ISSUE_TEMPLATE/ask_for_help.yml @@ -1,4 +1,4 @@ -name: Ask for Help +name: ❓ Ask for Help description: Ask a question or request guidance about this project. title: "[Question]: " labels: [question] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ae7046d..bb41a83 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: Bug Report +name: πŸ› Bug Report description: Report a reproducible bug to help us improve. title: "[Bug]: " labels: [bug] diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index e4c64a7..735acc2 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -1,4 +1,4 @@ -name: Documentation Improvement +name: πŸ“ Documentation Improvement description: Suggest improvements or report issues in documentation. title: "[Docs]: " labels: [documentation] diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 87cd7ca..90423a9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,4 +1,4 @@ -name: Feature Request +name: πŸš€ Feature Request description: Suggest an idea to improve this project. title: "[Feature]: " labels: [enhancement] diff --git a/.github/ISSUE_TEMPLATE/security_issue.yml b/.github/ISSUE_TEMPLATE/security_issue.yml index ee44028..31e140c 100644 --- a/.github/ISSUE_TEMPLATE/security_issue.yml +++ b/.github/ISSUE_TEMPLATE/security_issue.yml @@ -1,4 +1,4 @@ -name: Security Issue +name: πŸ›‘οΈ Security Issue description: Report a potential security vulnerability. title: "[Security]: " labels: [security] diff --git a/.github/ISSUE_TEMPLATE/translation_request.yml b/.github/ISSUE_TEMPLATE/translation_request.yml index 5e2b226..a156fca 100644 --- a/.github/ISSUE_TEMPLATE/translation_request.yml +++ b/.github/ISSUE_TEMPLATE/translation_request.yml @@ -1,4 +1,4 @@ -name: Translation Request +name: 🌎 Translation Request description: Request a translation or report translation issues. title: "[Translation]: " labels: [translation] From 88877a9fc03c073d4c7744fd7f260df9d97b6073 Mon Sep 17 00:00:00 2001 From: aa123456 Date: Thu, 17 Jul 2025 16:36:00 +0700 Subject: [PATCH 06/36] Updated Incident Management Overview Cards linear-gradient --- .../schedule-incident/common/OverviewCard.tsx | 5 +++ .../incident-management/OverviewCards.tsx | 32 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) 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%)" + } />
); From 6906a659527cd1c542da965baedd850907ce9b19 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 17 Jul 2025 17:26:17 +0700 Subject: [PATCH 07/36] Fix: Improve uptime monitoring dashboard performance - Reduce refetch intervals, increase stale times, and optimize retry behavior for uptime service and server metrics. --- .../servers/ServerMetricsCharts.tsx | 5 +- .../charts/hooks/useServerHistoryData.ts | 18 +- .../hooks/useRealTimeUpdates.tsx | 136 ++++++++---- .../hooks/useServiceData.tsx | 167 ++++++++------- .../src/components/services/StatusBadge.tsx | 115 +++++------ .../src/components/services/UptimeBar.tsx | 195 +++++++----------- .../services/hooks/useUptimeData.ts | 6 +- application/src/pages/Dashboard.tsx | 31 ++- .../service-status/startMonitoring.ts | 24 ++- 9 files changed, 367 insertions(+), 330 deletions(-) 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/charts/hooks/useServerHistoryData.ts b/application/src/components/servers/charts/hooks/useServerHistoryData.ts index d08a437..954f1b2 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"; @@ -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/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/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 ( -
- - Warning -
- ); - case "paused": - return ( -
- - Paused -
- ); - 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.source} -
-
- {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/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/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/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 From 0cad3746164a186afdbeb0555c66b8804b76d10a Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 17 Jul 2025 23:03:40 +0700 Subject: [PATCH 08/36] Fix server delete function and Implement pause/resume functionality - Implement the server delete functionality in the server table and dropdown menu. - Implement pause and resume functionality for server monitoring in the server table dropdown menu. --- .../src/components/servers/ServerTable.tsx | 498 +++++++++++------- application/src/types/server.types.ts | 2 +- 2 files changed, 310 insertions(+), 190 deletions(-) diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index c73172f..8664c30 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -8,11 +8,14 @@ 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 { serverService } from "@/services/serverService"; +import { useToast } from "@/hooks/use-toast"; +import { pb } from "@/lib/pocketbase"; interface ServerTableProps { servers: Server[]; @@ -22,8 +25,12 @@ interface ServerTableProps { export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { const [searchTerm, setSearchTerm] = useState(""); - const [pausedServers, setPausedServers] = useState>(new Set()); + const [deleteDialogOpen, setDeleteDialogOpen] = 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,29 +46,105 @@ 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)); + + if (isPaused) { + // Resume monitoring + await pb.collection('servers').update(serverId, { + status: "up", + last_checked: new Date().toISOString() + }); + + toast({ + title: "Server resumed", + description: `Monitoring resumed for ${server.name}`, + }); + + console.log(`Resume server monitoring: ${serverId}`); + } else { + // Pause monitoring + await pb.collection('servers').update(serverId, { + status: "paused", + last_checked: new Date().toISOString() + }); + + toast({ + title: "Server paused", + description: `Monitoring paused for ${server.name}`, + }); + + console.log(`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); + console.log('Edit server:', serverId); }; - 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); + } }; if (isLoading) { @@ -81,189 +164,226 @@ 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 ? ( -
-

No servers found

-
- ) : ( -
- - - - 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 ? ( +
+

No servers found

+
+ ) : ( +
+
+ + + 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.name}
- -
-
- - - - -
- - {server.os_type} -
-
- - {server.ip_address} - - -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores + return ( + + +
+
{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" - } - /> -
- - -
-
- {diskUsage.toFixed(1)}% - {serverService.formatBytes(server.disk_total)} + + + {server.ip_address} + + +
+
+ {cpuUsage.toFixed(1)}% + {server.cpu_cores} cores +
+ 90 ? "bg-red-500" : + cpuUsage > 75 ? "bg-orange-500" : + cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" + } + />
- 95 ? "bg-red-500" : - diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" - } - /> -
- - -
{server.uptime}
-
- - -
- {new Date(server.last_checked).toLocaleString()} -
-
- - - - - - - handleViewDetails(server.id)}> - - View Server Detail - - {server.docker === 'true' && ( - handleViewContainers(server.id)}> - - Container Monitoring + + +
+
+ {memoryUsage.toFixed(1)}% + {serverService.formatBytes(server.ram_total)} +
+ 90 ? "bg-red-500" : + memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" + } + /> +
+
+ +
+
+ {diskUsage.toFixed(1)}% + {serverService.formatBytes(server.disk_total)} +
+ 95 ? "bg-red-500" : + diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" + } + /> +
+
+ +
{server.uptime}
+
+ + +
+ {new Date(server.last_checked).toLocaleString()} +
+
+ + + + + + + handleViewDetails(server.id)}> + + View Server Detail - )} - - handlePauseResume(server.id)}> - {isPaused ? ( - <> - - Resume Monitoring - - ) : ( - <> - - Pause Monitoring - + {server.docker === 'true' && ( + handleViewContainers(server.id)}> + + Container Monitoring + )} - - - handleEdit(server.id)}> - - Edit Server - - handleDelete(server.id)} - className="text-red-600 focus:text-red-600" - > - - Delete Server - - - - - - ); - })} - -
-
- )} -
- + + handlePauseResume(server)} + disabled={isProcessing} + > + {isPaused ? ( + <> + + Resume Monitoring + + ) : ( + <> + + Pause Monitoring + + )} + + + handleEdit(server.id)}> + + Edit Server + + handleDelete(server)} + className="text-red-600 focus:text-red-600" + > + + Delete Server + + + + + + ); + })} + + +
+ )} + + + + {/* 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/types/server.types.ts b/application/src/types/server.types.ts index de0c42d..c346d01 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; From 6c227f496e498b660aa71d33bb87554ad4e70d8e Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 18 Jul 2025 14:03:32 +0700 Subject: [PATCH 09/36] Remove individual toast notifications - During SSL certificate refresh and display a single summary notification after the "Refresh All" action. --- .../ssl-domain/SSLDomainContent.tsx | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) 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); }; From e388912ceede8334a095ceba7c3a4702cd2e4a96 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 18 Jul 2025 21:51:25 +0700 Subject: [PATCH 10/36] Feat: added bash script for one click server agent montioring --- scripts/server-agent.sh | 460 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 scripts/server-agent.sh diff --git a/scripts/server-agent.sh b/scripts/server-agent.sh new file mode 100644 index 0000000..3435b3e --- /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 " PocketBase 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 " 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 From c1c94115e0f4846699cb11083eb46603e04ac8e1 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 18 Jul 2025 21:52:16 +0700 Subject: [PATCH 11/36] Feat: Added Operating System Logo for Server Agent --- application/public/upload/os/centos.png | Bin 0 -> 42745 bytes application/public/upload/os/debian.png | Bin 0 -> 39614 bytes application/public/upload/os/linux.png | Bin 0 -> 59587 bytes application/public/upload/os/rhel.png | Bin 0 -> 13576 bytes application/public/upload/os/ubuntu.png | Bin 0 -> 48541 bytes application/public/upload/os/windows.png | Bin 0 -> 6140 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 application/public/upload/os/centos.png create mode 100644 application/public/upload/os/debian.png create mode 100644 application/public/upload/os/linux.png create mode 100644 application/public/upload/os/rhel.png create mode 100644 application/public/upload/os/ubuntu.png create mode 100644 application/public/upload/os/windows.png diff --git a/application/public/upload/os/centos.png b/application/public/upload/os/centos.png new file mode 100644 index 0000000000000000000000000000000000000000..a946ebc6eafb2d8eabde67eef1e8de7001b984bd GIT binary patch literal 42745 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 UX9zt6M;O`6jjtJ18hAeXAEuzRY5)KL literal 0 HcmV?d00001 diff --git a/application/public/upload/os/debian.png b/application/public/upload/os/debian.png new file mode 100644 index 0000000000000000000000000000000000000000..9b025dc46c8aa631ec4c9b86e5b7ecadb8770d32 GIT binary patch literal 39614 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 literal 0 HcmV?d00001 diff --git a/application/public/upload/os/linux.png b/application/public/upload/os/linux.png new file mode 100644 index 0000000000000000000000000000000000000000..8a69099456464af44d5fd18b76c84713273b36a7 GIT binary patch literal 59587 zcmbrlby!tj*Dg$VgMhSvbax|-q#z>Qo7%8x*np%UB_I;gNQ5@%@bT|7f ze$Vqg@B5zX{PA7q{K2Jb?YYL9bIdVE+~ZzRue4O~aj0;RkdW|Ssw(OtAt9eV{$XJN zcZQ~yqk(_0T~!S|kdO#^AODb(ISHtdkkEP^^b9-=G&LlwU7UF=ZCtECJWyv>U^Ehv zlq}TM(i#l%WUvC+IY6XY4_i7|860e+S>Fn4@@cv%fb1Pq{oFyX{j~I~{lM1ZHmtHT zI8smvK!7vI(~<$|>;&PqVm6o09Vqi_MV=u61==V zK0Z7?f;=wncD(%J;^Mq~0=xnO+`tHK4_}C4Cw{v#dY!DIQ)9k0hncz{LoTH1K<^7HULPK$v->R;m} z0(fhkq&MpNoHMRw%a($kx)!$&*z^ z#of}z0g%P;(i*TAO-ok>ElY0)JIjB}N6yuiLBqw)g@KzvrY*+}RmyT&$%Pk^Ac>i~y|Hp;=Yi)p| z09*#|e;f;N@gLU&%t4yf9dJBuH)xVbNNa^J73K7xnR{)%Ni>FL%ePI$3AehpJS7Fc z%grX2{^}nJ;^E=N;8PyqS@2ol^#}58UUzoEQKjfGKgRn~zOU+v=o_{5y`aQ9?2_VZ z4N0^7kQS0{R`j+mMIT(E&#n6g1lpi-Tc#If6#TB|_y#a7P;y|7hnnHwV56I$fI$24 z?b31{5t)F{)o=7Nve@h-e9FIIN|mT;V~&?aV|#Q5(@(R}dx_=03%4 z=yKZvz|_YAP;gBq$U5cVPvJu$HJ!*GD_Qx-B`9|#oZ4J2?w9`h+(5c_!_BO(*QWVX z{IH?8S9nMK@S0A^T<3lK1>DdvbYPvir%s-Q1ANmLa`2b%fX=nf7xvU6e!GUwl*QCe zWa~$n-5zWxyvPV%Wq1YBsSr1`ZAFJDjNnl-S=RZucMQs%IT&;(T2|A|?>i#UNAnu6 z{VBRV%=;VH%{vW_g2LUVW*q+DMONainl`>o>eHTvG&{B5! z`;w3AVCC|A@K!Ja1HncxFG@QGl$Wo^FL$18{Sw78 z#?%_+w%h_`M<&|Pr=w}*{BeAnw;OSoH%a3(ay#;sB^-?a1>#()N*`sw##D1=wkaEcIZ zlo^NUW`n)ZN~LuxCM;vt${!KIgg3JBR@1|mf77AG*eXLae7k!cgm)|>i{8XuaYP=!mE07+0RY_qJQ;cT{D~n5MLpGw6fl?-ZyF6y4R8OtX8iT&LRm%JwBZ3m(O|V|!9>CBjjC-8bB!TM8eWthjwE@H zJBH#2{1Fw(8Hc*@EoLN>@zoi>Q{z15M=NqRrOw2DGYdk7RjMJ;d_ zC)p9IQbZYYtK=HOs+hKi#p2wAmZA=sfIVR!%Js&|V{o$H;%pcn;8?HtXGd%`PdW;J z-w;UFjanVV0dsQdaXxWO#yK~_A3be;e$Z7#*Z{tl*7&}lPFY2vZc6lMxJcnY=@A_8 z9P7LpN#6BmI(jKe%@y&BaE7_uX9D;4%)cEnp)^DFGfg9+Mxz{*oW0F6Y~jDDOe@yj zlx{mt1sMsOQSemLVFmEXj(U~cGMaOc8*-AdadGRI7;R)T7dwhD|Yx0U+CDlmYtTA7RQ%-7pIS1CG3Er#i~-P!bC~k zBAMYCZW&64F?HqokFb^V&5!NUIvnq4jU{V zQOV7o0x{*11czxck@6Wr|07nd^wCIM-w!-IJ_+fOv4=;ikeYFhHpd08WLe~g!=wpK zOWD@`PjFa2s0E?aJ^wNCrgVn>tBU~)VUR6v1 zw#6TG&e5>Br5X0_*5r6a&7;C$|`a8X!r9h#(C zoF?w^Az{bCIu^5OKK9G`#f)=Ju&9eOeuaq$&$Tm@OhkF!xPIjKD&rTkz_dE~T-phj zJ)BeD>H7f}v_R1_QMuWgS)ZUNhCRnn&|ltxb*{tWk?W0;ciB<-D&p=JTmd~jje%zM zC+Zd7Um<@nV@b7xmJe1)6`P9cRxa9FN4QYdZY8J{KTS(#%+}E5`~vt2XJ6{FQN1go z%8zaxKFoV&M1JVRKrusVvM3u?f*KfERM)<5?=4Ag4Jd{{_JXqK_3POxgM1Oce3S(q zqLY?YF*ftb<5tbn>4xrpY7Cfg3MgS@Yloh;&zmMXynAVP$i?z)8Nw;27i}cKu>}=Z zo)Dwph zv(Jm8e^~OS!BAFtN<(kw?3UVN`#7Ft;hxTy=OuD;s^{(#y61Kv&U#9>?UlUeJJ z^wQBaU2W|Y;Zhc+FAZLZ6Bv+(^?FO@wI5#@rYJ#NKi$C@lIw&yHr$6i?C$Y1G3YP^ zQsF<^Zga6OnIC7&*a!{6FLE@XA4%WiHr`NU|8TLt)^*vDqhX$dIHNOST4uBaFP_Xy zmCQ6e;i9lzUnm{=BwaDDO zUTTh!Cb#~({*plYuF@gB`en(4e|Tk#DLA&;IcZt7=yhIsl9j7xlAefq)hj-92P2yF zg{8v=K5m1CjTwqCGdVQVSxy5Wc|k-)*)G>-NGFea>>Uq0%}C?lSAUOu*%))bWH1AH z!(PcnicJ6gZo1x@H2Q<~hWk1u1{)9WYgldTUEBD5AbBHJt47UP1;d`v!AR-n${E+- zr^C(LLc8F1Dm}a;i93f|y@I2k1~(2IO7FC`@mGNZ?cz11*ws)~uyqWAd@WH&2fL8=_E@YN7?6Cox0#QLl zkbCIVJLn82>#_}5-Tfer%am5wukW|=mW@- zx|=ccXN|2CLMS&XN*V-jq@@C(t$@`W+kD>W>)SW@Yw;`VKGn1SYg$ZXbLB2ziQY^& z9tVNkM-!B|r5p2m-dz2?Ro25&ca_8i&g2?RGqaQT5JPBSalKZgM5@=VCyxpjU9syf zx!G00@TJA{$V3@o*uo8i8oz{K&v2Sx zgV#Mtz4n^ejXI^A1T}4=<-C|#eFfn{eeAb}IE*+{bhw~u8-e8)AQRsscVfW3Q`b1s z9Riu2_Vm#BuVQlrWl<;&OUm^y61q=<>*Cnf`bQxaHfS?mhNec2zN*6`t2L=sN{0;P zK9&@ZGBC*c&#!4aFD;`(YS$e{@$g(%cZ#Embw|3Ch9%{hC<&|!^Yz|xd5wDgJimNh zs%PZzm&ChmAr(UwLH|lg(RADGQ7ggdQfPN}A4{d(qtkVx^>_xN>~c)s{(95f+7q^r zH($?=zuQbJet_>1lOytlp3=hCGQ_Bk%J%h;O1KjDK7=k?K=Im$hC z_VOwg&XAkf5&V6D(r}B`t0hfTjx;A5Qm1_K+m-`$u(M+`3rQw)kKZavv9!l9l?qcf zO8l}{p2Clc*faMF_G0$3Moom5=i8R)kMuIbN9_Tz@9c?B4}KH549OSj(vGlUDVqAz zMcI6Q&#JxJyOt1<%MQN{F-LWeOy)V`TXm=K%aS@$?u)OKQ+@XG&;B`vJRQdJ2-(l9gN@X_0ko=yBX|dG} zb>R9nj1Yg^)_gjqoe8PP{&O8UWUObZsfr5jf zi+Z0=VHaBa+Njyn%z{r@>};vze2K?W!65Q0ON5VjNm#Z5muD_TRY-9NeL-Ke>#<(+ z>qB2cEiRVw3%Y`F*(F{v(gWC^r~vlDv2t~ejKaoN4PR&O-$X*_?@(UL9eC_#7c)3f zRE>q3F-1Bd{3$8y)-$w#*u6RYbTv?@Zqojcn`(D65az>~@ZuVANH#wTaixhU>4~Vy zGu{~^yC6gzAZlnCvyKesIitc6L%EZ(-IszwhVb?m2HIa2cb+mU!Bh<2DlFHJ7)f~U zZI-SSB7aW9LmG;SN%-)UeOp{(`7EBP+$T4J2-ukCp)s3;7Ch<14f;Su!4gkDa3c9F zD)o{l);&ngvTx1wxWK;iRO*6OTCKhVQC_he22a06*5D}2w2&VKj?Ed>CGs-m4Pu%% z)ksi4S~CzjJBk0rqVW$48ACBilrrwZul@2J*tn4(zjW-rn2yK9`^lfT{3FJpRw|*D zYd{=q>-VwBi^>gu(RhEkgy~sS_HFEGXy6$~!uGa=A2ewGbesO^v#cSdB|ErMH0tuM zEGuiq*kfMU7K)zP*(hMj7~NFOb+H^I8|7)_{>z7WrCWd_!Co(6mpKHy9MzX92CXD^ zTSl=9sVM6E6%;xK0nVuzLnCD%HlHRg#lYei5fdhXQ1pk$)I&Ndbc-bU=OnbEK0P`Y z@2#!R@Ch3n3l&O_=|aZRRMgs1KDOblyX9c+oac8y?4R^$RGr56C(VGaRKjy2|;vHq45=+i;;srSsz24JTH-ur9+AB?QE_>?BZ$`)(6~+r({(Q zchEmKIb+Kg5<2lpYs9ar4BqAqqv%|ke91j_%iJbDB;T|>!?r*JTx^8H&rxP9XIrwX z+gew-kHxwgG_sK1kORYQY9&K|vwUHDqPqQ#%Czi4;dw7=lm;Cs?^2(YdTKI+v~db3 zZ|2aGOCF`;z(6dH9ox9V=Png(cc&@j(XL9>)pbN>Qddnbt$%Y@cv{7UR=#;u6x_x7 zCR=4_=S}puLWC-F;$*(h*u01u`@{^j>#i3E7@QQNG+0_XsrG!)k9O3n3Mj?hB8znk z1fG6wqB#bN=Wl`?U2033-E^LUxR9dSX=t_zOgFc+SdG{Lcn#+Op##TFai@}H+T-P1cG zoF7~(EUZnE>5*+Cq3^|8gJcJC{BXV|r=%KNWl0)>xdw9eIo!_AXVO|cqx|X|6*+Aj zT(HgITtkd08UlXXM7MGDtSK6+%?Hh2VP?b^N3qIKGhCOBk>t1Lg(ht9Cdh=;(%<8w z_^fI!r-5g(0^Mit2|sHVmr%s)mLargVw6Wc7dH1rm)8fIJQ75T4W$oqkUB{G%!rz! zh3tsEN$^`KuE;f#_?(VeCaRrT;%J#fZRQJo{>m>?8nd_hWzC&#`K}4(tW2NpHlE+q zqb>@;^-If!NBDhqNDRn#-q8U-L2TPZUi*Q;mG~m(>1ifnajWs-W>-(WP#JP5;vKr> zo20Mb=(d7VXq8|ip~*#O?xS0KYk(#_fec_B9y8@>UiaJ(t=D6xorg$E<_Kam!0NJk zPJ$ku9pMq@FJvi(?qJd}q^Kh8cl>Z6n<$(!9}G9U=i8{7dKG&MmTz+QOc;OQMUJqST6~-zRRBi zJuc;?$sR8Hd+h#w9`fcfugACdB0uK)IgHFSYAde`+w4Kx8uGkJF) zJ{T^)6_aJJS!l4nSqC<#*9^2l8;$=OaS-Eob&uhOS`n~Hx|p8jTpsW9$q&{)IFeDu z!_Chp5DF6IHQbjXkF83|Gb>G-{%(vY>j`nqK{9)EEsvfli=h!o2I-EEMNJGEh-=JA zAbm!((=JnUF^|kom{-T!aYpjR{2eiX@B9`k;3pil_Jt6Po^DgqIvW&MdX)Ia%#*~t)WPy+s3m`)Q3JPLX25Z;#^tiAp=L};Hk64Q#7$nNAE{N<(-ko5dE|n8SSL#pCbWaiL z4M9RP1I(0!-S3qOKcZOo(By26-hO!K5C-aqcZ+3N4dW7oxbI2hJc2y{gM3Ej6dPdG*g@Cx~;jkHzVhst4^(OS5$1)5~5oql|twks=@(nKX? z@SB z)MnKynDB|tQHDC{Fj~dOS4qLk5dHV=0KhR}%jA#K0v0RLbHyZ9(fRiw{z zx@jk?-lng>IW@lM4`e70u!csjeIso;+|koUzXvH!Q5UQ0fjrv0KL#0>Qa2G1X)-)9();N(KZX4 zYP^{TFZgVza3fQr%O*n(R+W^CimPagVH>Y{#P40&j!-t_bGg^NKTmCdQYgqN(^KtR z7NoxhK~wKKkv8fcLDTFl2TAf$z){glAbzU{k^L!iCM60Ze=PYzbu2zbj_;>91C_GN zu41Tu@r-#;magRYYzIw_96MLXXgwKipv=>U=^7bY9{+sm{OPwcm>bwTrMp`m>~*BjSUz-j zzKf@=<;@%pb4#=~ET}eo-KNc6Sf?c@Cg%RY+3~wK_-=T#)_FN}i~~p|8jksrS1=-V zX|=#W-t{d*AS z(4?_*HJNi$xwtN>rnT|M-$9(TckSa}+D*eUx`qCdrOMia47K0wc#Pgle0|U>^T>Vd zlQh3{EzIhgY>Ol7N%*x@wRT+JoR+LxHaz-zN%>rG$Su`(ofyc!>>Im8IXm;XVjhm& zPMDYg0~$8wZ5?w*yPp3|0)&;b_*f+eW_Oh&UHehNLjA4Hdanr~?<>CB!GHFC74AqPE#iw~P!-Ox}`sx6IaS9#3=s zh3SR8j?M+nmg{nW-q1$BX2kWx{y_e-gNH*c#;%gf)&Z`P+81s5`Hj`Tr}rX!Sr*oHf{B;^-k>6QzVrn4^0kk*st9%3J0FCc}unbI*F@346aIhN~%b4T+|JlSy;H% z0F&-4caMy8_M+YA=PPF#i;=6b1O7Gv74ru^ew|Q+ZfS@YcV3;c;dEO0fgaHx0Ef+8 z*y~YjR0#7rgK~tgVfNPHf((K6ZkLz`_67t(8!d_JSB0_af8yZ*m^UVOmGFaU@ggzr}PQPgpPHB{)GAN#5@MTN z?!}@|(Fgx9?&O~c=kX{0Tb57G-J1$C!tQ=xRoS>3t0Q#?30yGXKu7pUsl8&8MgNo?@V5?u7$c$BfYW&?>X*T({ zeb)H7Unu=GF_6vwv)?1gMnB}@pOZ*Wu@x*GAQUHsJ%b32$?F&J_Ko|i6d6zI8{UL2 z^wW@?>F>__$T`4-4F*qjTR^|M{{R5nwutqEbpfYErP&g=c1@t1AAFMVy@oxUG(HiC z9okP#MwwDHKGV{CWcg;4hzrYciBSFvARe%fRr`icwD?9L)^iu|<$#T3o&!T7k;knP zNoXl$8xnxb(vh9*lO&mFUS0~e1|gXn-O&zsrJ!7lv$>g+&;fJ#aV z%{966$1?_JaVD!>u4hpA;R5DmY>!~2_fh~Ee)|Ug-hvMk2Yl5|dpq|NYqwQ5f@~R} zs&*#T9X+MY%q(!h4Tpp-9v|ncytJ+HzpZH^RdeXnH7eF`$wcMF$^cRsPg2~-)@|x_ z)&I(*l5Tlb z&b&NPo~Zcx6cZb0D{-m~!nqkvZKjS;rVhJnGtZ`;n?DNy*4CMRhyG0u{nS820~(vq zY&+eBJK=+$YXd(C2?KVPV1d80WE7^`%!eE&^UMtlu*~wB`S9gt$pF1v&JF824W!o6!TubuT`E^N z!+xG!daPFsd`nT&o1XqQ<0w`|dF(mAZtv3A>VTC{z|z!M*%Kvf9V%^C5>^*UI8m{Y}(^mYV|$)+ZR)TOyO|B zNu)#39Ea6B=A38lQ1^kLQK)V!O$bKLg|HD>xM)5h`paBQcI=t1$YR}GkZ>YyAjSua~=?zpq=dd15+`9`1Wzm&9R&2;YP+ zuM|%#IwP`L(<+@afW2jMkrWV~s>%}@tZqEMSTJdEeMZyrRYVG9Jva#CPDpwEd6woj z6_9_39tW$f!)p6?gP|OSl};G}tlCsFx2C->*dN=;QPu-S-+ljRc7sn=a(FIqU7|Wk z=+d%nJ#B5(t8fOZ#a!>injU^UC#V;qFC?4NjoSUfvs7}eE;cKedL)WU|DwD~o+2Sp zb6ceD8#OdL;v&CnK?$xuZInn)>s;X2M!oY8NFaTpy5=Qms=6UkC6b6DJHo4aiHrCK z=O?qDjj^gQ{h~l9-TR~Ky$_jJAzA$zpRi!hk?;=%)q{TlIO?rgn>xzwNHxbbv>D;`%CSF^B4@#eN@Y9=t{?vd|&1g{<5iXmB`k(lCd~Qb! z*SWcIzec=FAtNKs*W6)&K}bw{r_MP^3{Ihd@!r;ssR_;>DJ$yEaRFfL2K+stu8En* zj@7XkkbMBH{04n+q$X>)^V*0y)8EAgy(OdAlM{grtAx73=hG=)*q`7e;h--X31LQ7 za{+vZ5|cPj8SUnd96cTtAX4xsgh9TAHrY$~;B|`Lz*~ipM*q{$EDNcHN@cB<` zf&78Gd|9ihP_!r?-3q{342%tP8*6)kgs-pg`CuR8pQY*JL(k!E2fJ*QrdMB0*j#sS zylT>NT(S>#nU0>{=t$A9B&dSf)V1woTrp#O>GPEE^22FnQ7gx$Qqp z2{*2&@m$03_`*{KJ$IZyc}qP1z1}Ae$(HtyS4kPH%5Rr^W;BaQQtZdB8aw%FZm>$oc@800>G@7n%O{WHyA&LR0=wQJ^&FKXcqPow~p z`#?M-+%)R(Mb)V>CSwVX>a&4=&LEcgrA_)ARRXf3t5c<1qa!-8DJXURSc(g$sU7dq z`oJ?l^2pD}-vv~0B>?nQO%-cv6#Z!#`AZAPFk$!pRxEW=SZ*7qWs8G-&zp>I2SN98 zjC;VL9&f*@?rZ^Nu~sDEZ%Q99*~eOHr1WhGS&XzfPyG_~Tw=(>11O3-?ia;R6cm6y z_h9E0l!?}P?YO11(}%q|fazHA%s?wA%KMiB(I5QBbb`&I1L4m+lTR9Bd2L$j$1cWW zVuPNZS1Tnt5Q|HZ+VXC$z%xzuU{c73T=u4|xxT-9o(lJE#boA>%_ElKZB61w*IMZ`17$cC!Bm(K;&gv#Ji6kVgB1V6PYC}vpb93|252kEwetW~Q zi}M@HM;WZ?<`A`dA|T3R#=JB$02~CSiHV|NfIncoA%&nwB%j%6>Q?}{GJWL>g`ggo znsH(+JSKz~II5_Exd<0$g+c(lmZV}_bcNlnHs<-7I-^`dlQ&G(n(C#M)!<3bT%%wK;IYF zv7~9Oi4mN`W(Elfwqz{uQkC)+!wCT9g(oU5Ww*gL5Ak<9_Ci$~0ZCnM=Ow>#UAXUJ zqi0l8!yhZ(-KB>4Xq*S^66n}R$sAog+b3)H9|bl`#Wr0%7dJaAM_zR>rd3utTmP6>6F$gpg%36t#k9lQe>clV z3^5a=>FZTT%sj;%D#5`|{lSBLfW@hOyPfh-`sr5gdxGmsdCu??Av0Q&x1vSG(yus7 z;4EZWAmo>Q_VX)0#4O&|lhsuZT<`MaNW5LM=AD5)=w{75kGGopxaPGByY>BBC?7H- zbNv#hSgQ)reydTh@TqwkHrge7xcbwkG;1c?{xS*8QV`Il7O7aIw&kFOks{H#0Z{Fv z?WM>E9E)y&@A`&>WzLmjzf?4iZ0tgs)&lC~He zVz+{O5gS9{yL-LWvTF4}9c{I{jV7wP=D8rz>3QiH(7|t@50pi! zEe)C;-#tI@b(V_P6Nv#W=-{BSfAn!A62szR7(bB51mcuLY=ndEG26%$j60G3X_sy^ zS$n*@n z)6Q$3`lxr^-_o}4by3nonY)-UY2De;vx@*&ddd4o?4OZ$cghihg?E%fnr#>&>2_NE z(9Oo4(i$Fm|C0Wk)>j)7TaILdex^z9SR37r8JivgF>Y=pr~RchD&wI7QKT_CQTj2< z-HOdQ$Pb*a!pj$Pa4s1hlkF=E4YycTpw9b!e-F)`aeD^FdJl3tZrWnw;;9dyA2h$Y z5Sk^I3hZ6EZI|k!$cdZCsUIsBk>6IS&S*XP!_G%OtOOowU%R?$94Wz$IJ|lhJ;O73 zwIR!SESdBD6-*TMX1>h`$ab1;Zg$UP$FmG-I^`Q^BY=*L8|eA(&4)Fn)J;Xv1sOTH zqo{WYPr@SCgUyTPFaw5ggaM}GHqd%Umv*)0EHp4QfWs)v>@zZ?*)FWuBNr-NeryiW zN1xUN$qvN_@D06>pdo1&2eT*g(qZ}XM){_8T4A2HOX0_TxMvbEzqR??aj6$-qE=O1 z#I7&&romTmf(O8A>(|Ca#b%#y(rV1q0SU+NZuz8P7ZK!HT>7CoQ&YslP{02nViLG1oHFT`QeP7dQ1a zhrdqcuiit@P#oFE?4@SVCTVG^u8uo0R$4lQ`pmORPsX<#@8&4J5A*msdkvTaoj#y1 zOH!gYFq-&P*J63>6=%W$(tYP!Fm;JOFlF$FyUW$T@Y9C{FqLSjqTd2$$M$L0kmAQH-l@2c* zW%0Ee-)N9ETzUIY)|;$!8rs?HlI&ME*uBT~I)czgighVza0*}!1NlEVT%J?m9p_Qa z531&|XT2Ldo;bJ@xHq@_n`f}rK-_E-<9Cc~IV12xY%HMuI$E?!t ztgoJ~i?24dk6pFZj29PcdA+4t9OuAS?!2Hk2*MFHHo~Dtl_4;JtWBcjh8S$i7mszK zrh3w9hTZHV0oj4mKxTH9))Y#Oj33y${<-Y8LWYCR+k=?Xee;QTEEH+ML znnN-mS6@x1R8PakUvYP_jJ_wLgR!sU%Hl&3PAIz2{C=D2c~+jz_c!lPgi6GuJcd_x zZmr%;yvA-nUM_b%PoWTJY&R>d(r5dGP1Wcrref|-m-a4Ok42&CRit}6nv!ihmKd6e zqRF9r#!f5m>qd%WKKX|_Ay^{J)S<2);N0R9`aryQz3mh&oI&A^wRcvXWynECKgDFv zH4X1npt9`ZQTYX+?Dh8t{tFsF~78uxjas`XjDjjwTDSdaLS z2O-yR#kpF&_@zpO2Q0b;yL<}qX>;C}`DS1s%IBH8I8d*%8$qh(2xdj?k53T{c8VNl zT1r0-HLjEg>N~wwIO$&=t@9T@yU_WjUup{5FPDyQ?~gi<(J)x zG5z&05{WLNBPOAWk%aPkYeB)-n83GTI_v;D9_G}4OCKpH<0Lv*6aC30Yg3{Ho8_*k zIB_KKkhuTbl%pwjCf-FczcdEGeA4^kf3$jOxp)BZmFA;?lLT>s8}s=#B%lUWD=6T7 zjL&a*1t1+l%u#gt>keXkPQpAxjGN=w4RX>V4*5~!{Y0&L*Q@h8f<-zQA_-r4n-^$5 zRstlS&R)KsFPUl|bLXttPO&2&d*ynU>(Ig^t24M9P}3+~LYj4qU%o+IJXrKtBt^T_ z(~$!T`iF>@1ruX^+pg$ru$MLh2@O-7IqiBI zuu?BBNqZB*GWD6 zfScJ@Q1#Qxz#i9~5qqBj>*|^ZzB7hskO>Cy3kd^9V9Be_n&+Y9oy<)=6**42%WWZw zs5+e)6Suk|;2;NnOu&tzm#<<3ayo1V@02C{j7YT$ha)TvzZsNqUT>?hON}zgimBFD z@H!?1RMH4XMu}oWg3&FFf4pR_MGcRdzjsLA(ktLJkxDBFvO?J|Po~ClEvl$^&S}bi z)e9C5JQCF}@k=){jrq15{sC0}q^h8A!2J6ODZOF7ZVcU|tvJCBu%jT$x=PP#TCN3K z3(wj&IZd%8G?#2Tj>1^ov_K%QkLfm`1o%budRPI7`%4u~R7dLhQ&%f~peRf@O{)`` zT79U{etWfr@J+@UF<~Y?UvOxidm=2lc7t9($-*YCl>_ zcgOefle$Z1ntbp5)-!PS5mE^KEYb35-(^%cT{m3LZ_zB?6k}zBTMlFOtFi%Q!-NmE z2~*XOYZ)G(+Fu{fXIn5CB>PH(dDl6p)x7+^%KBJrN-ew_W~?JZsO?B%&@&082ulG7 zb+ZKI%0q!&WU zPW0%=?p_+8KGshURNW!~^~^pQZgqy*>Ei+o1So-kj%gYZfu#16g^uPv=`s6a0J@}a z432eR%p%M5AK2KZ<~a;t)8g2qCkk+($mUOJDk=a%8#MOtdg=|+`abThTfWFcf8tayBzpgJSn~4#eJn=s=YQJ zZGM+eB72-5?09fd=MAv;0EJQIOxf-`c)_OKIG6TR69;V#({qZ+VR7<&;`qZzGr^;z z$1TiQ0Bx@J1L{Yl4lJ7Dz8@C`0zaz;=m)CzX(>Eln#(5u9=+8Jpa{+$8jV6*$E(I( z?I%v3!=}#GmpQxtc-DZe9AeCGdV~YFMPnKl$UF+kfR62Org%+L>g{_>@D8>41SIt4 zpnmPEbo>VS{E=0=q#L|6WEp*vnA0>8fJum+gk!HiSi&>-w_dHk5Cf>C551%|n3$4* zT}wD!N>5=s)YSQ|rz{)tmKoOF#;0@@<|U`Y@7+#bPVMP?5JvjJZFwhG?1CzO5F&mV zouQyN5UNR~*#Ii|yyIytYgvnsNRk(itNPtgGs>+wv`EJC+d&^_2IzAnh!EbFhhPTF zqU#guP%r|DP<+;S^p5E$k@Bs-4L>HOjB7B!+UY#Sg-jXMqv_-IQK&an7|*z*~VNRifU5T<|- zuY3Z^3eam;`IYIQ$a)!?iizgnQDyTPIGq40h$i==&vPz z18Fq;VLw;kZqtJ9tnncStt|AfoCU~_ZxNV_*O z27}mePo@1ypajtOwjxmst(*X{T_1~a-Y(eum_9$IY4i~|ppmi{IpCMW-ORsdDBgoJ zgomU5o%(yxk6ylf27QpvV%6$<`m4n2%eI*$0qpC2fj>;RVKANkexFhHGNN$5WsZM$ z;#Mnrz4QKZt?8f$*)`+WKr0q#0XpS4V{!d6 zVF`dK9oZiGT%KIl#V2bbnNVLmmcz3?CpJvf$OnO~-4(A>&{L)iv=0@DY_Dv` zqWWI2>qGv0ym{ym9VBqYxWXfvi{M@K2YQh{OkA7vR`=q2$6%a<|}SkDdC_H>AW z(%6|JP#1hx#Qk6;#f1`Gnk?8c;(^ zE+2S97UzyBeUma{Jgw6WzbvLvQlYZ|N!Ma^J!QGPzANz=6I z$#~PhlR*jlhg+Mm)Uh5iC@UG8sHs0LwNW|uGfFCFfJk+qSebo^+Iea1m(`Gzhw+L2 z>B^V_A*>XmVIhn&K`W7(%BoN^Po`n-`1q}3=DdrbpzYeu*qhdYc+J_7aCI;q@wOTV7+&8kfNj&dJXC3PY?}u5FkuvI!ciy;fYj07BwDoSWBZ?kzU zd}E>b8d4}c!fIZ6s&v_xy1i_g2Z@U*I_RFc+XX%3Y<-_>Xt6!W;Z+&U1}#UC0(ELN zsNJo+i*m5~Bfv(g2klAj6Kb$_IGNJ}44i4B3NX>4Ne;?~ka)%&r*0O&M5wVB_Yh=P z*cujEL&W>Qrv}6M@dB+b<~{N|Ub)Sr_&VB!l=B@q-FNRMQ5YL?g}Evm9?l9mE)RXg zgs5Z|+kDHqAs1j#>GoY&@~brp$A!0)9oY}L5%d6eBf+_8?y#A20b?i>a3Xnhaelmu z%>Wi}0bdd3bKGnxaLJ&iPQ;zO|Kc9d z@&djYd(4WZ-0hcdm&M{h3txpLus-j;s91n2cSZH7Zk=-d`c=O=Hz!sy(MhavcKe$p zh~nT8utUOqt$y=`54;(YL2J}%0n{n(SK}%paQ#O4`Ge<9BYmG=Vz98Lb-Zgz{#xv1 zkKY%)Icg3&zOsx^SR=@u@jvh>Bx4)V)j54G+caD}I#tiR85mwiIx5w(-SvJKt>Sh0 zkww-*^uS20THBwhdvsBG;2Qu|Iy;BJKw|}~1LSyXl#M|8Z;W|7fTtH?nju7z#eK&) zB78ROn8=~T@nvnBRQuDgjl|~fMfPqA2i61&Prvl34AGK${ikIi)33fe%G|t5e%~W9 zP^^Jx#T(f?O%*F|oGxl-`q0(Ed(?#MwPw3qlsZ6y4YaX)jzUm(As;2?HJ*FQd^f<8 zBHZw$mDWa`Y1#5EzQ9yKhYwvyFEBvn)9*a zjKRUPLXitaihQwNJKHKL?zu!SEWJ>JwYY;VS!3LBxZvWg>!s=QrY*3-Yz!s9&6*c4 zBe(<83=Nw0lmflN$s7B(SHqI>x2C4e%aMxk8mFS2^WU{L*a=bCBnhfI`)b(7WOrv4 z(Z5)_`Zws-rP7zPY`uHJhDgaf9;O@ueWgr2r9BqN+HAM9jBamEs5mbVhr!DPb~Rj; zHDk$RcQXFFUvrz%44CD)Xn2|(EU;ZE_|Rxx6i1ogUE8Z0w7ITaC95j zPNBMQ!wBPPqm>ch?9pR! zGt}#mri<$JVoI+~J8{}9W~yw-O&3jp#Fqe5TZYYWl-OPrcWB*$P+@7cX@^YA>g>!X z;BA=gTvwzt=;zl(S62nn*7Z>eH{0U& z=w>i{^9F7)w%8o&Fq0ZQf0Wl&6n?R3%&uJ^$fG#s-Gy4R-lVQC1qjWK~|m);1k6hp11ESyK=r z%OF$>^h6+z{M`9zl$7+3L5r3`Zg90JHT~Puk;)?JT~^BmR|E{x1io+`{*%jwr)v6f z>EhjQygitgqq)g?2mNnWx>vFeo6@}+^_XaX|44-^D&pGvCtIYq7G3~K!uxCg18EES)3OD8xrng?OV#1K%;kulM+wfv-s&hr{hD&Xa^ z6GIs}?(CEL??gW)CjM)?7>-a+!ZwBs!3(E-&$6cyTp#214TQfRoV1o91;=R0wu50j1yRv z;DH1~CiB!Ld;hz~7tY$+4{7Nh>e52x+Lsc`$Hy+^a8iL#)tlMkCvmIy;=nS6g#Q+M z`4*4=grSj5kg`SXI9GHKs_T$dTJAXMRgFJnm$f!CDMp=U;hlT`>odhhICQ#aD|VUZ z>BCp6!yy!f^Hmz07HE05UUJ26&Gq2S%GNaE+hdA_3D%?1B?1Ny+LRX&T1&poo6j7C zW^)LJ|4cjo-1(^c2*eD^Vt<`O`4ljQnn{>f$^#=})@M?n4FjAcCsq{-D{|OZgx&kS z)RM2|f?0bObH{6n5TPCa_WtEwHKOJAcIdt=Lp^Ot42WK;aWmJTeckqS^Aw)#iznA|fzcQUzC|70 z{KQ%m*3ZGLtgU4vj8s_;K{~GsZjtAwof}=tv5iZPtct1YfEwxt{ibWIlYvbgIs5zkA;jep75~o zb!9k{Gpst9^UO%}A9r^<*0a=v6MG``{d6}_5wz;eQCn^2%nXMNtvEy2bPMa~Oiw)4_*e!6|Z+jP!a0@4R+5$~tL=^8NhSqw6qDfK4d9 zWt+L?!o)FS$3MybXFcg&s!rYTy72M{Ia-L-!Tu*a3*TobiO8PN_>475gWd(GhuIA>Aqa@N*+m}azpc_~?_ zLpS`C8C4B~*ci7s_fFtLB&#Bqq1*;5&vMn)%>%HNqR3)`*mTqeq!x1@2LE6?;Z%@; z@EZPbcg~Xl3x{`$a0R~(<4=m~Xn-x|Q0;bQfyCp`gO4^w0t#fANHFubj18^hZV7}j zg^nlXzHBG43wks59x})6u97LV2Oi)gQ>PM+ooBV9yT09RD@e+J84*ZgG94|`2MMO9 z?loxj{hk&er2u^SEe5+eDsW+qi|M!d#uGDZN`C4%Kq{Up(x`+kHI_TT6ssDXhXvwT z;rJ?6@>;BRiDo)mZAdjxw&-Meb3_55#A&hES7RLR6=>H>WPP zQ$d%5CR4!AI;rYBFxd+Jw{&AHye`6{${6_ssPbr8Ejtkce=|*uethFuPvwv(PQqUY zIYSm)@sdF#3}h^epN~jb#F1me5opr=5{o8P*?X;z{<73C#pkRv2cDT{*s|Oc8C{IF zEcLYcw4vh0)&&yAX;02ioP+jt+EUYNZBb_CS?>AwW<%8p(9#442mPLY7sZt>efCMB zu|WEze>>w}uEh^5%|jv+sKuZ*pv3X@<6G~Aw9#>)3^?r3=<3RYxxW_ zaeBDz@U;cv?IrYCE0sFhQ_ZZ5^yJBZ6&Om0w`25}|BcJIUiFplCt)bwUbWL&rn#Z$ zy)W`GB;11bGIbOq`s&`7@!(7vZt~#3#B}Z?YVmhBb0#_nf%Y*mZL<8 z4r?X<{_d_#ZSUHQ2(vW0lz^4^GwO{5<+ZBB{E)kp6 zB+nb*l2IoE9;{?1UEhu%{A;LLFZSLgdc6NI#nEE$H!eZ7T>4>1qc@hg%KU!(#>3s%oSh!u*O@1{&WPtnUZLQ*2psjKc5Dc7wCP;i z-uK@f)|E2LnCn@JYcCJYJj7!}X(bCA!9|;!*{$oSCt=1AG`j`Sx5t|y*i(AHnHE+r z=&(oP6(~M4p^EaS->olq728p#$}gbCJGuE6!#hx@gQFuvjq4vB8Pi1ZJNR1V*Y-fo z90VWP+mWO-Hyn6kZraig2NjlqZ;@Az$XP>k+@9`i-luw%gk5{SzQzz>S;1|@4hvam~->f3v+!l`-Rwtz2J?LekF~jg)?|lE5%lNfOWlA z;`F(}7{2olA-qa_@!yPmAJ|?N^-aKIqS_)-XZ z#E~IOORS1<;W63qHcg+2|~09kV3_n=10apcN8` zPJDjv%YU>Cw`ap+1Kg>im=F1(p~%tEIGADO2Ok^>kX}`OHSJjtEiYRi9_N-7p-gWN z4l5M&EACd7mnu+JkXY-jIXq^jw@Yv5<8lm_Yn`qXPV{Yb0?oHPzI~W%1>Fx0@|`uvLvNAb%L6;ju4q| z=cmq4q-S^cJmj+`|DD>wT>n8!pKywJ#-)NPXp;)E%SF)i6Niz0LzC58QD}3CBJ)4;!M>v(>8?t6<72g-S9x{eX zy&|9cYfycemo*K>O8$@ZJLCZzfMIQ#k<8AL21*X2W@3)|b0OLbQKIZdXtX6JiT=A> z0>?V*-=g>&N(bxPnY8VmYj0~SYYM#ZzfO++o*s5sS`qa7YN1YOqq9IzVPAlM0rK(^3nZu+I5 z_#s;Iw+)`T!}|MF>5%HhQy2(#4x;x1R-sVt863Q(m*XkAdC0?Msjl_(f3KmT7vtLp zRnI@S`<$g#2I~dkpQtjEW!v+#+#=^c`r6-25Ud2rM{>_OjxeWOs>@X|C(e2EB>5-y$7vtoV72ldr}9N}ke%_t{j z7DKdwkMgz)s~3Uwk0hkw)s!_wn>O2xB6Y*SIAk;+8vP9Y%~`paE2i?u)oJbT2<(-M zTfCXzX+E9hR*AQfz(4V2${~_+x(_;;NqB*B+m9bMa7L_lLVRj0NxvaEw#1a-GSq^`0hiDyu^w4XvegAM$ z0lad`1^yge1D8uTvl(Lfax`CUcV%I3F>l4Gxxv^LY;9sT-IHh<>Ga;cd|X=IlBOG< zd8bs-)&93Hm8HO6(cVH^xgU7@d7UucKh83-=|E0kUZhOhUAaVurC@Vy&G@PrD^4P( zT*oxr9EWzzL{qr@acNgu2ArH6Gc~$;MZ&_DEA$T4cxW?sD?UKJdrjxL-te68dy^Lf zZ(set+bxG0LkL2%iVdwfA$-OQvsYEP`0}#M4`@H)LGZ9}ouA^qd~x7%57!Zp=!ooS zhx=+-seMeq^88`cy~1+zDbOeRV!3D}N1J0l~A&bDZ?;51l zWR0WGplPx=NC8}M^-tQPBcn*U)pDU^p0&|wLHcl^MTwrx{JMNmRCyXGyb5u90J4&n zlA(%2YSN?{H|~YQ{=(3@2O8{%(Cq$sbivPg2zk=x{IbF z^6sak!NPVIl$+x>vW)1*eCm8DX<31f@r~TO_W@YuurGv7D>h>LU~OLX4%fn&kg~f` ziP2HJw!5{Ju3~(-$QFoYm8;ds#a|?UzIdcg?K6}7%i>E4m(oo`sc`C1 zcs#bPH&NAmh2Ffp1?jmIv))Ee_4q$0ZqKz_>rd}j-aV6FqRRsFNWo_qGgxp5|ISlr zjWL)*y!6#^jvU*Cl0J%OYs%n_eyX^=Uh5AHa+ zt3KRIu|7DU(@mgMyop6+iDk!EGfU}Y8vZOS1V~;#4=UNRad700#%xx=Zr)M?%<8Gm z82!${#zx+PAYWlYVe-IGr2oK#m199!<%j6;0>N~9+aGfq#YoW6^gsQTTNjr$`xsF| zFPu5}EKN2;7jMcxG~w84R071V6;cF_evoG$rsALa{B89iU8o2QBzBykzWv&ROXT+b zZ%TeR!qz4$K(9G`A2~NCC!a@j6Ru1{ zh!$d*4oLcU_ZhnJ8>grnH~QX9pmmR5=%nvArD~QP^?7sQPff7ejcZo6by*)-yPkZ_B!R7&!8@Mnbt`I#ROxB%&^Jj zrFke&z_$S+%f{jMPi?B@!P|FrjN^!MBUrLRka zc}B)d8;u;2k*T7O-N0{NPfGK7?qh9Mvh9xSt-&`u#>4_z=x1zW7pq^xG)h?MsXgRq zTjj;WUgP5)D^u~9mNF$V=;jR4AMl}-$F|Eeb5erX6hzGbZim=sYNqnOmj|VPo$rI; z2fE-Ka*)-ZfbYJ-%34)sUBq2s9F#~Pz@ggS|Q-0&ZyECziqZWIXNpwyhF+YdnUbYtcfNrg2)d;Vze z(N3=;(av8-K3?#HhZSc{6LguJz(Bat;B0|qxjn{JJ8DNm-pv^UM*9LCbGCW&XC@Qq zo7o-KJdI?r$ff)c$)@7OfD$Z@aClhu=*+YtA5CgPTP15blyF9R>DuO_fMduy zurQ&Lh2c^yFdK5|J8y8NDTaDoYzpI}$_5>C<-GgAf^bt>x>8{|UXpb4`P8x#Rzp!(CWmI#5c!aCfz&Ieh!V@rPOXDN!WI|10gyW^3j=;`8 z89?!OCE616LkW#gZ`FVL7PdR$$WlN<-2+Yf#$}Ou&r0v24T;tPCvc|vSoA)TVW_W9 z{bV7Y#^8_kEgIZK9FQ{B8qC$%8Pe-P%q**%RBZFKgiWG0LOowswKn0>t`QxVhAaNj zF1l;S;|wnFJ2_qb%ajfaD0ZKf3drcFLXELf()Si}qFrt`#9}M84B!%My0y;>zW;tR zV<+q~HPz#HhU?};;o|wCz|l^tnD7>=)8Q3lma=C4y(6)dUH3Mid_5Y_Xk%i2rVI+} zh?2`m1PDIq1>x;0p~YjpaaY6;UMY#?2w=T;?zXR|tIismHmDw%8`_?pP8IM?jsxa5 zg6!be#tFy+lTqw!{%sMiph=)h9MJxR z8_FumYD-^Rb9nUjAt2bdT-<;mB|W+K69!(aQ}olF6%qA?WGYBdOpuI*EEp3T<2gs&eW_`@4@BgC&bCg zX@qn*Xg>H6`#6_tQJRkFuMu)!O>r(ET(CYHQ*$+?T5gvrH8l;N pDsShN~|AF@D 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 literal 0 HcmV?d00001 diff --git a/application/public/upload/os/rhel.png b/application/public/upload/os/rhel.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd5e8b98af031729ff46df0a85c4074633f8c7e GIT binary patch literal 13576 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 NxpdW_M$aYoe*m!0r^x^S literal 0 HcmV?d00001 diff --git a/application/public/upload/os/ubuntu.png b/application/public/upload/os/ubuntu.png new file mode 100644 index 0000000000000000000000000000000000000000..fe086e29b765ac8044d0aa6099ff6f85b2bdc998 GIT binary patch literal 48541 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 literal 0 HcmV?d00001 diff --git a/application/public/upload/os/windows.png b/application/public/upload/os/windows.png new file mode 100644 index 0000000000000000000000000000000000000000..cefd4ae04028b82a3e0fe8521f49c661955b7ba2 GIT binary patch literal 6140 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 literal 0 HcmV?d00001 From 868832f5ba2d747e077b2706bb1d53eee847a491 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 18 Jul 2025 21:57:56 +0700 Subject: [PATCH 12/36] feat: Add create instance monitoring button - Adds a button to the Instance Monitoring dashboard to create server monitoring instances - Include the one-click install command in the Instance Monitoring form - Display OS list for select with images and names. --- .../servers/AddServerAgentDialog.tsx | 167 +++++++++++++++++ .../components/servers/ManualInstallTab.tsx | 125 +++++++++++++ .../src/components/servers/OSSelector.tsx | 51 ++++++ .../components/servers/OneClickInstallTab.tsx | 101 ++++++++++ .../servers/ServerAgentConfigForm.tsx | 172 ++++++++++++++++++ application/src/pages/InstanceMonitoring.tsx | 18 ++ application/src/utils/copyUtils.ts | 60 ++++++ 7 files changed, 694 insertions(+) create mode 100644 application/src/components/servers/AddServerAgentDialog.tsx create mode 100644 application/src/components/servers/ManualInstallTab.tsx create mode 100644 application/src/components/servers/OSSelector.tsx create mode 100644 application/src/components/servers/OneClickInstallTab.tsx create mode 100644 application/src/components/servers/ServerAgentConfigForm.tsx create mode 100644 application/src/utils/copyUtils.ts 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/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}
+                
+ +
+
+ ))} +
+ +
+

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. +

+
+ +
+ +
+
+
+ ); +}; \ 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) => ( + + ))} +
+ ); +}; \ 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..767e73e --- /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/checke-server-agent/refs/heads/main/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 + + + +
+ +
+
+              {getOneClickInstallCommand()}
+            
+ +
+
+ +
+

Simply run this command on your server:

+
    +
  1. SSH into your target server
  2. +
  3. Paste and run the command above
  4. +
  5. The agent will be installed and started automatically
  6. +
+
+ +
+ +
+
+
+ ); +}; \ 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 ( +
+
+
+ + setFormData(prev => ({ ...prev, serverName: e.target.value }))} + required + /> +

What is the name or label used as the identifier

+
+ +
+ +
+ + +
+

Auto-generated unique identifier

+
+
+ +
+
+ + setFormData(prev => ({ ...prev, osType: value }))} + /> +
+ +
+
+ + +

How often to check the server and metric status

+
+ +
+ + +

Number of retry attempts before marking as down

+
+ +
+ +
+ + +
+

Auto-generated authentication token

+
+ +
+ +
+ + +
+

Current system API URL

+
+
+
+ +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 7ad1fc4..1a9c628 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,6 +52,10 @@ const InstanceMonitoring = () => { authService.logout(); navigate('/login'); }; + + const handleAgentAdded = () => { + refetch(); + }; if (error) { return ( @@ -102,6 +110,10 @@ const InstanceMonitoring = () => { Monitor and manage your server instances in real-time

+
@@ -117,6 +129,12 @@ const InstanceMonitoring = () => {
+ +
); }; 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 From 63d63a908ab98042f850f2fec96144a6cac6ec92 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 18 Jul 2025 22:01:26 +0700 Subject: [PATCH 13/36] Refactor: Make instance monitoring page responsive - Make the Instance Monitoring page and Server Table List fully responsive across the whole page. --- .../src/components/servers/ServerTable.tsx | 331 +++++++++--------- application/src/pages/InstanceMonitoring.tsx | 20 +- 2 files changed, 179 insertions(+), 172 deletions(-) diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index 8664c30..dd1c43b 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -149,11 +149,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) if (isLoading) { return ( - - + + Servers - +
Loading servers... @@ -165,8 +165,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) return ( <> - - + +
Servers
@@ -185,173 +185,180 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
- + {filteredServers.length === 0 ? ( -
+

No servers found

) : ( -
- - - - 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); +
+
+
+ + + 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.name}
-
-
- - - - -
- - {server.os_type} -
-
- - {server.ip_address} - - -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores + return ( + + +
+ {server.name}
- 90 ? "bg-red-500" : - 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" - } - /> -
- - -
-
- {diskUsage.toFixed(1)}% - {serverService.formatBytes(server.disk_total)} + + + + {server.ip_address} + + + +
+
+ {cpuUsage.toFixed(1)}% + {server.cpu_cores} cores +
+ 90 ? "bg-red-500" : + cpuUsage > 75 ? "bg-orange-500" : + cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" + } + />
- 95 ? "bg-red-500" : - diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" - } - /> -
- - -
{server.uptime}
-
- - -
- {new Date(server.last_checked).toLocaleString()} -
-
- - - - - - - handleViewDetails(server.id)}> - - View Server Detail - - {server.docker === 'true' && ( - handleViewContainers(server.id)}> - - Container Monitoring + + +
+
+ {memoryUsage.toFixed(1)}% + {serverService.formatBytes(server.ram_total)} +
+ 90 ? "bg-red-500" : + memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" + } + /> +
+
+ +
+
+ {diskUsage.toFixed(1)}% + {serverService.formatBytes(server.disk_total)} +
+ 95 ? "bg-red-500" : + diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" + } + /> +
+
+ +
+ {server.uptime} +
+
+ +
+ {new Date(server.last_checked).toLocaleString()} +
+
+ + + + + + + handleViewDetails(server.id)}> + + View Server Detail - )} - - handlePauseResume(server)} - disabled={isProcessing} - > - {isPaused ? ( - <> - - Resume Monitoring - - ) : ( - <> - - Pause Monitoring - + {server.docker === 'true' && ( + handleViewContainers(server.id)}> + + Container Monitoring + )} - - - handleEdit(server.id)}> - - Edit Server - - handleDelete(server)} - className="text-red-600 focus:text-red-600" - > - - Delete Server - - - - - - ); - })} - -
+ + handlePauseResume(server)} + disabled={isProcessing} + > + {isPaused ? ( + <> + + Resume Monitoring + + ) : ( + <> + + Pause Monitoring + + )} + + + handleEdit(server.id)}> + + Edit Server + + handleDelete(server)} + className="text-red-600 focus:text-red-600" + > + + Delete Server + + + + + + ); + })} + + +
)}
diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 1a9c628..6d9f7f0 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -61,7 +61,7 @@ const InstanceMonitoring = () => { return (
-
+
{ return (
-
+
{ toggleSidebar={toggleSidebar} />
-
+
{/* Header Section */} -
+
-

+

Instance Monitoring

-

+

Monitor and manage your server instances in real-time

- @@ -118,12 +118,12 @@ const InstanceMonitoring = () => {
{/* Stats Cards Section */} -
+
- {/* Server Table Section */} -
+ {/* Server Table Section - This will take remaining space */} +
From d40d5898c0745ca03fc575622f1d38e4be5ff473 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 19 Jul 2025 15:14:04 +0700 Subject: [PATCH 14/36] Fix SSL frontend notifications - The frontend no longer sends notifications. The backend is now responsible for sending SSL notifications. --- .../ssl/notification/sslCheckNotifier.ts | 139 +++++------------- 1 file changed, 36 insertions(+), 103 deletions(-) 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 From dc586dc15fef2d925796696baef3d9e814de7b27 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 19 Jul 2025 15:23:28 +0700 Subject: [PATCH 15/36] Fix: Server Historical Performance chart improvement Fix: Improve sidebar collapse/expand performance --- .../src/components/dashboard/Sidebar.tsx | 8 +- .../components/dashboard/sidebar/MenuItem.tsx | 16 +- .../docker/DockerContainersTable.tsx | 2 +- .../components/docker/DockerStatsCards.tsx | 55 +- .../components/servers/EditServerDialog.tsx | 680 ++++++++++++++++++ .../components/servers/OneClickInstallTab.tsx | 2 +- .../servers/ServerHistoryCharts.tsx | 34 +- .../src/components/servers/ServerTable.tsx | 349 ++++----- .../components/servers/charts/CPUChart.tsx | 93 ++- .../components/servers/charts/DiskChart.tsx | 93 ++- .../components/servers/charts/MemoryChart.tsx | 93 ++- .../servers/charts/NetworkChart.tsx | 127 ++-- .../components/servers/charts/dataUtils.ts | 113 ++- application/src/pages/InstanceMonitoring.tsx | 10 +- application/src/pages/ServerDetail.tsx | 104 ++- application/src/services/serverService.ts | 271 +++---- .../src/services/serverThresholdService.ts | 85 +++ application/src/services/templateService.ts | 41 +- application/src/types/server.types.ts | 1 + 19 files changed, 1534 insertions(+), 643 deletions(-) create mode 100644 application/src/components/servers/EditServerDialog.tsx create mode 100644 application/src/services/serverThresholdService.ts 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/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/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx new file mode 100644 index 0000000..9c6b618 --- /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 + + +
+
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Enter server name" + required + /> +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ setFormData(prev => ({ + ...prev, + docker_monitoring: checked + }))} + /> + +
+
+
+ + {/* Notification Status Toggle */} +
+
+ setFormData(prev => ({ + ...prev, + notification_enabled: checked, + notification_channels: checked ? prev.notification_channels : [], + threshold_id: checked ? prev.threshold_id : "none", + template_id: checked ? prev.template_id : "none" + }))} + /> + +
+ + {/* Expanded Notification Settings */} + {formData.notification_enabled && ( + + + Notification Settings + + + {/* Multiple Notification Channels Selection */} +
+ +
+ {loadingAlertConfigs ? ( +
Loading channels...
+ ) : alertConfigs.length > 0 ? ( + alertConfigs.map((config) => ( +
+ + handleNotificationChannelToggle(config.id || "", checked as boolean) + } + /> + +
+ )) + ) : ( +
No notification channels available
+ )} +
+ + {/* Selected Channels Display */} + {formData.notification_channels.length > 0 && ( +
+ +
+ {formData.notification_channels.map((channelId) => { + const channel = alertConfigs.find(c => c.id === channelId); + return ( +
+ {channel?.notify_name || channelId} + +
+ ); + })} +
+
+ )} +
+ + {/* Server Set Threshold Selection */} +
+ + +
+ + {/* Editable Threshold Details */} + {selectedThreshold && ( + + + Threshold Details: {selectedThreshold.name} + + + +
+
+ + setThresholdFormData(prev => ({ + ...prev, + cpu_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + ram_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + disk_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+ + setThresholdFormData(prev => ({ + ...prev, + network_threshold: parseInt(e.target.value) || 0 + }))} + className="mt-1" + /> +
+
+
+
+ )} + + {/* Server Template Selection */} +
+ + +
+ + {/* Template Details */} + {selectedTemplate && ( + + + Template Details: {selectedTemplate.name} + + +
+
+ +

+ {selectedTemplate.up_message || "No RAM threshold message defined"} +

+
+
+ +

+ {selectedTemplate.down_message || "No CPU threshold message defined"} +

+
+
+ +

+ {selectedTemplate.incident_message || "No disk threshold message defined"} +

+
+
+ +

+ {selectedTemplate.maintenance_message || "No network threshold message defined"} +

+
+
+
+
+ )} +
+
+ )} +
+ +
+ + +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx index 767e73e..1067d20 100644 --- a/application/src/components/servers/OneClickInstallTab.tsx +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC = ({ onDialogClose, }) => { const getOneClickInstallCommand = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checke-server-agent/refs/heads/main/server-agent.sh"; + 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 diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx index 274f966..688c69f 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) => {
-
+
@@ -71,7 +71,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { } if (error) { - // console.error('ServerHistoryCharts: Error loading data:', error); + console.error('ServerHistoryCharts: Error loading data:', error); return (
@@ -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/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index dd1c43b..02ceb43 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"; @@ -13,9 +12,11 @@ import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Tr 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[]; @@ -24,8 +25,10 @@ interface ServerTableProps { } export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { + const { theme } = useTheme(); const [searchTerm, setSearchTerm] = useState(""); 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()); @@ -104,9 +107,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) } }; - const handleEdit = (serverId: string) => { - // TODO: Implement edit functionality - console.log('Edit server:', serverId); + const handleEdit = (server: Server) => { + setSelectedServer(server); + setEditDialogOpen(true); }; const handleDelete = (server: Server) => { @@ -165,8 +168,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) return ( <> - - + +
Servers
@@ -185,185 +188,191 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
- + {filteredServers.length === 0 ? ( -
+

No servers found

) : ( -
-
-
- - - 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); +
+
+ + + 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.name} + return ( + + +
+ {server.name} +
+
+ + + + +
+ + + {server.os_type} + +
+
+ + + {server.ip_address} + + + +
+
+ {cpuUsage.toFixed(1)}% + {server.cpu_cores} cores
- - - - - -
- - - {server.os_type} - + 90 ? "bg-red-500" : + cpuUsage > 75 ? "bg-orange-500" : + cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" + } + /> +
+
+ +
+
+ {memoryUsage.toFixed(1)}% + {serverService.formatBytes(server.ram_total)}
- - - - {server.ip_address} - - - -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores -
- 90 ? "bg-red-500" : - cpuUsage > 75 ? "bg-orange-500" : - cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" - } - /> + 90 ? "bg-red-500" : + memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" + } + /> +
+
+ +
+
+ {diskUsage.toFixed(1)}% + {serverService.formatBytes(server.disk_total)}
- - -
-
- {memoryUsage.toFixed(1)}% - {serverService.formatBytes(server.ram_total)} -
- 90 ? "bg-red-500" : - memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" - } - /> -
-
- -
-
- {diskUsage.toFixed(1)}% - {serverService.formatBytes(server.disk_total)} -
- 95 ? "bg-red-500" : - diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" - } - /> -
-
- -
- {server.uptime} -
-
- -
- {new Date(server.last_checked).toLocaleString()} -
-
- - - - - - - handleViewDetails(server.id)}> - - View Server Detail - - {server.docker === 'true' && ( - handleViewContainers(server.id)}> - - Container Monitoring - + 95 ? "bg-red-500" : + diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" + } + /> +
+
+ +
+ {server.uptime} +
+
+ +
+ {new Date(server.last_checked).toLocaleString()} +
+
+ + + + + + + handleViewDetails(server.id)}> + + View Server Detail + + {server.docker === 'true' && ( + handleViewContainers(server.id)}> + + Container Monitoring - - handleEdit(server.id)}> - - Edit Server - - handleDelete(server)} - className="text-red-600 focus:text-red-600" - > - - Delete Server - - - - - - ); - })} - -
-
+ )} + + handlePauseResume(server)} + disabled={isProcessing} + > + {isPaused ? ( + <> + + Resume Monitoring + + ) : ( + <> + + Pause Monitoring + + )} + + + handleEdit(server)}> + + Edit Server + + handleDelete(server)} + className="text-red-600 focus:text-red-600" + > + + Delete Server + + + + + + ); + })} + +
)} + {/* Edit Server Dialog */} + + {/* Delete Confirmation Dialog */} 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..20976fe 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/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 6d9f7f0..c8dc476 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -98,9 +98,9 @@ const InstanceMonitoring = () => { toggleSidebar={toggleSidebar} />
-
+
{/* Header Section */} -
+

@@ -118,12 +118,12 @@ const InstanceMonitoring = () => {

{/* Stats Cards Section */} -
+
- {/* Server Table Section - This will take remaining space */} -
+ {/* Server Table Section */} +
diff --git a/application/src/pages/ServerDetail.tsx b/application/src/pages/ServerDetail.tsx index 3dee273..1a432b2 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"; @@ -24,7 +22,7 @@ const ServerDetail = () => { const { sidebarCollapsed, toggleSidebar } = useSidebar(); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); - //console.log('ServerDetail component loaded with serverId:', serverId); + console.log('ServerDetail component loaded with serverId:', serverId); const { data: server, @@ -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) ? ( + OS Logo + ) : ( + + )}

{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/serverService.ts b/application/src/services/serverService.ts index c808783..0f25142 100644 --- a/application/src/services/serverService.ts +++ b/application/src/services/serverService.ts @@ -7,7 +7,7 @@ export const serverService = { const records = await pb.collection('servers').getFullList(); return records; } catch (error) { - // console.error('Error fetching servers:', error); + console.error('Error fetching servers:', error); throw error; } }, @@ -17,190 +17,151 @@ export const serverService = { const record = await pb.collection('servers').getOne(serverId); return record; } catch (error) { - // console.error('Error fetching server:', error); + console.error('Error fetching server:', error); throw error; } }, 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...'); - - // 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)); - } - - 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('πŸ“Š 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 }); - // 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); + // 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(); + 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..18a8162 --- /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/templateService.ts b/application/src/services/templateService.ts index 85d5536..fc68e6c 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 c346d01..90e0f2b 100644 --- a/application/src/types/server.types.ts +++ b/application/src/types/server.types.ts @@ -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; From c098ecb6bfd9628e2b9844a084caaa449ff19931 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 19 Jul 2025 15:37:00 +0700 Subject: [PATCH 16/36] Fix: Apply server table background to SSL table - Apply the same background color and styling from the server table to the SSL table for visual consistency. --- .../ssl-domain/SSLCertificatesTable.tsx | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx index ce6fb15..75df57a 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,28 @@ 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) => ( - + {certificate.domain} @@ -164,7 +170,7 @@ export const SSLCertificatesTable = () => { {certificate.check_interval || 1} {t('days')} - + { ))}
- - {certificates.length === 0 && ( -
- {t('noCertificatesFound')} -
- )} - - +
+ )} {/* View Certificate Dialog */} { - +
); }; \ No newline at end of file From e5afe276eeae2703b85b9e3eb17844b8043927fa Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 00:57:04 +0700 Subject: [PATCH 17/36] Feat: Add Heatmap button to service detail page - Adds a "Heatmap" button to the service detail page, displaying a monthly health heatmap of the service, with statuses color-coded as green, red, and warning (yellow/orange). --- .../src/components/services/HeatmapChart.tsx | 174 ++++++++++++++++++ .../src/components/services/HeatmapDialog.tsx | 97 ++++++++++ .../services/ServiceDetailContent.tsx | 2 +- .../src/components/services/ServiceHeader.tsx | 145 +++++++++------ 4 files changed, 356 insertions(+), 62 deletions(-) create mode 100644 application/src/components/services/HeatmapChart.tsx create mode 100644 application/src/components/services/HeatmapDialog.tsx 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 */} +
+ Status: +
+
+
+ Up +
+
+
+ Warning +
+
+
+ Down +
+
+
+ Paused +
+
+
+
+ ); +}; \ 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 */} +
+ + + + + +
+ + {/* Heatmap Chart */} + +
+
+
+ ); +}; \ 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 ( -
- - -
-
-

{service.name}

- - {/* Pulsating Circle Animation */} -
- - + <> +
+ + +
+
+

{service.name}

+ + {/* Pulsating Circle Animation */} +
+ + +
+ + {service.url && ( + + + {service.url} + + )}
- {service.url && ( - + + +
- -
- - - {selectedRegionalAgent !== undefined && onRegionalAgentChange && ( - - )} + + Heatmap + + {selectedRegionalAgent !== undefined && onRegionalAgentChange && ( + + )} +
-
+ + + ); } \ No newline at end of file From ff19d46391c0d845c3bb0dc49a155d50da8255c4 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 00:57:47 +0700 Subject: [PATCH 18/36] Updated favicon logo file --- application/src/components/dashboard/sidebar/SidebarHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }) => {
CheckCle From d0ecaaaf8fe3c92ccdb990d11b0b271afe3707dd Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 01:29:24 +0700 Subject: [PATCH 19/36] Refactor: Improve chart color and style - Fixed chart colors to be fixed with random colors. - Added another chart style. --- .../components/services/ResponseTimeChart.tsx | 453 +++++++++++------- 1 file changed, 287 insertions(+), 166 deletions(-) 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 && ( +
+ + + +
+ )} + {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()}
)} From 50c742d82d94ca410d988dd5e20df94e4db979df Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 15:26:33 +0700 Subject: [PATCH 20/36] Added new favicon logo file --- application/public/favicon.ico | Bin 46108 -> 57958 bytes application/public/favicon_sidebar.ico | Bin 0 -> 49929 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 application/public/favicon_sidebar.ico diff --git a/application/public/favicon.ico b/application/public/favicon.ico index 7ae5c720ad60353831cd826babd2f54938ee5d72..d5dfc625e98e0e0a40b2ea32a72be6dd3d447628 100644 GIT binary patch literal 57958 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ΠmOKPHXLX{9Wv( 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 QTLJgq)l@1~Fbn>F08L%5yZ`_I literal 46108 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 av7++uUuMnWsx~xae2S;vzUuEIF8nV7E56MD diff --git a/application/public/favicon_sidebar.ico b/application/public/favicon_sidebar.ico new file mode 100644 index 0000000000000000000000000000000000000000..074b8f96e658e5556a8bf93838189246ece3c57b GIT binary patch literal 49929 zcmbrlc{tSX_c%ObFl8BQWEp!z3fae2*=7%gD1au{Zhey`*G+CK@bSyJ@X$l;|M|wtSIufbHF=ToTs!9G z5pdb_SlBfjSPg;b8-?NA+fZ&=Z-c2g(nxB6VD$GFPFJ2V*oB3Ep0W1pixPro(nEiVR z_+_Brg~#JiDk`C&p~phij|Bu?RZ-K`)m2eFu5$eNQLy4@P`E$dE$pa&kSxH!A@vuB ziD!^|pf?Wh9pEp;#OZc9AQ*3;pui#ZZ`3F8_(1Q=|H1AbbjI`L9iEJ+J@$@vmQg-hX$(xdjG!+J)mh4HU4RK>@*m?w)@! zFoFJ!#5mB?4e#k;cwF_k=22DcqsLYCRsNS(%su`Kz%&ex@i5d?SJP5;yR3E8-Q$YZ zQ4KZiD@S$RRF5B3)$!2Q(9zOT)xE6!A4LBG`JZUaE(0u@$2E^@Y3iy0cwJ4M|JwQQ z%m2CFE+E+5i^+=N@&DlcuV;VP>Z>rN>*xJ1q5S>vZ>jtT;$I^B*TVm%mjC~%@^Jr; znsC8^et%8C!(GMG&-0q6Kc1;1wSOzg!ySe9#`}5xmx@mM;s2+KfbF6D-2AT^D1;sL z@Vw#{?1xt{H1_gz_ciwOlsYN}B>4Z}=-){6RsI|P{{wz-|IMnIcK6r0m=*_K0A|$x z%?$X3IvI@j3J5f`3-EK3I^*eoRiC-$|9%Qg{V%QmTS5iC_#dhM4R3{Ekh`1)SCd@595<$p!`|H{a}cmuHjL;Qo@?8BIxv6UPvEte5>>r{zE2X%gdM*TJv7Y%IB+oMSs^fO_)yKIcSPSfG^>FI!a>Jk5ny(|Xv-@ymBb%L^J7s^u zC3FV1wqZHK+o1^PHo?GdwL^N({+=B5I+r*gp#BHmQ1!rc_*{65gla7R(Jk}j26F|y zeZSb(xMu8De?QJ4%jNLF;^i=+;kxZ?S6+6UaTQf)O=_4wFfU2#e)^&`_|ebhsin@6 ztrPvHFyt*Y9GXnY`{d1nldfO9ETmdHl1mcGnN_WqrUY<(qoZQruWWkF-wB;2zlkFn zljXRTi-HKcv_WB=ZdfK&cMo$1X+STS-$v3xO18WP^)XllRVGTAq9h$%XGfOL1&(>r zq^kOR{fqXl0t*VRM=CQ)48?WNkbAh8f_sCp>ekV{{MonO`OIbuFLfvzvS()fi*WQ$ zDr;7i$;;&jN{%l1~9)T>prPRY2$XQ4PVZYDZETi)I^lAZD8WI57dqa8H4hgCO+tuAi(0X^u6SqEA4 z$H9rysgD>oW9H}P_cxdM!_VBEX3L67QklqBJsEf9yyYfV&bz8DJo!5N=`tK6Y9@BE$RxF7SiH`h z{3eUJ+bFr4jdtm<^msdMmKbzO8zX8NNgQGEy?E8&+^*v!3G`Fk;4ofEsAAgVku#0l6(518N#$%G-Kx>X$(vqSj!s= zt8~SaW|1EO@CZCLzaaIe?~s&WS@ zhjDuaS%(?rl}M5#WHANJG--@adA2jR;|MBgVLeN3bzNgZy0DZOPn{`v8!6}ge&gg> za*wz!2|9ctpyz?G9-1sV!MmWk09k;Xt5*n1mE%6bq_uuRBV|dSG)oL+QAq{cAV+m3 zUL#8h8Ni7d24s8-la#f4-Q4#cUx%rxg^}cf-vk54FfNIRTseplNsYYr_UATM<+$Gi zPDF3$I_(=gvtMlaRv((oK80z^K(3$deF)&OF+g5j(;t3DG3xIXKRXx$$6(X1LC!-$ zZF{#1TG9L)qgb*AIS|F^z&^;L%+?b7J57$eT9&p8h_oo#G66bw`pn!TTDKj}KS9jv zX6|9u8p7D7ag;<{S`flGePF(q(6!7Pj1D|fsS21>?AXufyH|=jZ_^v^K82nUVp8#C zLPMw~j$iXWY}_3VeC=~1AyUykxGYIRmimIpW|2yf?!8NAhf`hVlrW;%PifkO+Ts}) z-CZtN0?5f5*9J2lVT)p|;g5+&lj9~;agqzyO19Fb)ySxLNvlH`*e7rK1+}&VV(K>m zk*H5*j7v10;+b0jgXTlvODlicUTEHDkz^}}wX`5Wkg2&!j64iTNChPP!&H^Ct|d?d z56cTCmv6e~0eyp}=Oh!sdtGK&`tb8&+qJ`=3W`8PSyG-5bNEdu(Kbs>>)8Wud!B-@ zKgbfr@<2?_j7)*W@G98MZVy0rS=u1PP=yG?e9Kbp?8+ z6G(7Ksz{A?oRLmDxA7+igMBdvZ{Uo|DH#@FLNCC|<$|AQ%flpD{9(s9fMayw^w`T- z8)g7F6SYE{x8a1=y@$jgCgmEUU%zZ;r;G!}Nrpgl!Q|}wkX*KMZf6+`_Fm)JN8QhE zmh6QB3oK{_Tjxj>P28(oY?)Rh%e}mFi>9quUGXbLLe~2SPz#kBD9SmwFN)oP7g!%9 zBzWHjV&OnZggt*x)~xQxK0v-2f)%BdpsF zHRKs&%|)KT!p#frV+SY5{Y_%)Df;$tj7}h)AH=ZmWRf(4RlQ zhGNQwf^W1gPI34ZvR#~N$v@1(FE3>b6FG2jW54|IhiWgNX83*E`)sFFo~vb;Pzi!* zMhoWR#He>%o;RUdyg5?e&4qC_H~o*C`NY8@qjR_GeDpo5w!MvPi-6Hnk&&GS^Ygk> zn`o=}exlgwz5rPN>`L=SHu4G3GG1PgJc=W(~!0eSv?E$PE zY$+*LX>ynw%gQ)SoIG_W6rU^ycFQuDTlf-0aQ_Qe_HCadCUT~&Wcr|Ywl1)%#@;$f zoX{Ng@ZO^gId6S^nB(c%{PDNowfN@SOXRJ>$$H`o;`rkA3Xo|_iwK=C3yh}|K3T(x zN$(S%XwA9v_XG+6P1vhtc_i^E^LX_5J3!N`bL}cH!~Ic^4H82X)Kr%1-G(a4vKb`0 zTG!zq#$C$KlJH|V(FHZvlQ^IuQN$5KYI>V?ficBq0vO$;(#bhOI`I(A|Ct(D&Ko_% z5_QkcKM9d$Mgp?9M#}ng(1GKZSdI*aZ1%2~CPE3c$0ukg0iu2u_C)Q&L2+UDM1U6K z@`M}d3$VIP0j##vaLIwnMRPiE1n(aP98ovzj@?0|#r*?tTAp8KVRTn16#U|Kl*N!+ zKE<%aoKzY~1+UY<>$mTr`SS%NF%_P2Ams7c3$7vyeUv2}5XBj=D|NDfpaQK(VrpP! z^AJu5Cr#Z^LYaxOBgDxq_U(~S;W1q?Q0~?4!NZKGJ3;t_o?zxH>vlbu1%YRj`O;g* zku&PH-EY9=n>M0x*G6$T0;e<=*g1k=ZkkoTN}D1CVLWf(lY2Coz{bENl87*-YROlo zp$rY|YbBJ)3_nnN!PB9XK#95iia|GlW0ALhx)5wJ6Thc8}_*MROASLgWo*gBIjO%r#0J{i-7J9c=-c0i_wXDqee*5w zW(=P{WY}D3-7Fq`YZ3rz^z5)RuncadcrkCk!gAT}liRw1!XNA4(4zjDop3`~)IJ`* z48Y4T=Yd{vZD7H-7yvS?cSmcWz$Nrw@~aGhNJ?dF1Fe1g0bXwO)E!wcWo%3fVsc&z zfhr7E_WMTTll2$Dv_DX>N9%^Yoz62O&Koc8C>-q37w*qnABaBEFbwVId!XIC?KtrI zlGz{fAQ{>t}uY0&}9hwT^NpHJxJ=~o#IpVfVAJ3pkbZ-Acp$D>4l}Ragw>}b$ zvFp7%`x%4NT$t`Pb%q2(p?>|Ndkc*6T)xt>``mJ=mvQTFKf|aqQH{Qi*_3N|u{oa< zJs_ZH9OG_}lcwaE$d%by;D(moM<2E%)p{4LR-cs3$RIj_U^DDz82t-YhuCl$m{TLa z`DsfD48h;lumu+Mrf3rBof^HXTIqx%Y5a~h6+3#K#c24KL57P-ocD(+SiV9_%E$Rg z#(V^o^Ed0Tu~Ic~@bADlvDH0MtP2MQ;Ley^%YqA%G^hGL4%Y+Jox@e9$MYt4z2kHC zBr09zgI&GSH5xi#hP#8dc@*g}nlx2x;G9@4n_)uZL&~=muF6C$*B$%$8*Dfps=k3AjJo@5dyO z(7SC*{c_%MK98(xyWB4?=s-4ZxH@~>*T_F6mC;=wOL+={ilS9}5HX0*P0)R-*>$*v zb3|BTC4M;qI5zp{!r**&c2@+Vy#NUl^QPmViim1wt$~Ig5v-9I^%? zY+TPcEl{3Elvf+y$+M>#6YOtmVVZnlIC)${)9W{%f&Vgkqa+rdO>=pF z*txCcQ3Vtt$-0hiDw-T3-Xb;;WFuTG1|mjad&zdcK@@rdWaEwVG32eu4*6C|5w4b~ z$+5J*I(SAY^}Reswit=Kad-h<-zTI0$#{UQKQP6J^liO@upjhyFnzeVZTlD~_Yn4(K}u>xCtQ99Z8rDMQBFh?f%44Xg1 zUnb*wfxBFNLe_eb)6jLn1tD>L%E$AtB63?VU78>GZ)mY98Y2OPL|&imaJoH@y0#culJ6IwEXKkk)mo@Myb{OO8agliE0Jg36z zvJ1x8a4juN*NK-+RjE(7CE{Uv@7Xc1YCaudhf_+OCaqpOfXJEDtZH~izQJDar(n%v z=es)SHGIXCB@bLGrMx&?VW9sy*Mb&f;@Qcr6GI&0)=EYs1U>9JyZfI70RPzMOEgl*@CBS10?yjcw9m60W1TcFLcj3P z#36Cq*LwPS2W7qcd+QVy6sq2itGd?qOL?Dn;sQbat9iWF3dpzvTAK4Lj;FT?@}w+t zf-mt1X5z^-sI%}iuUN$Fz&{9FY61hbx|e-%`PeAS7np8y#5&1nB(qLJ$=Nw2d%kjSDu=Z zM!9x=`FB;`cn*yS-0jSbM7xfZlrPgDUafQu997J1+9qg5X?>CSxpj_#!iFo=n3K0k zCvOnA>cql4aiprJ~L$eb)%#v+V zn}KirBH_LDn_Vcqt!ui?qwe$F61a{*(v~n zci=gELLAfP+Iyo|aEim^{NItHH7Ox4TdL8X?tUq9!DrelpjPZwtZQ85X|KMr(ONMR z{{Z}w0RO|DC^g(^d|VYzSy0a2%hGL7JyC3w<=>x}HlR~}F1c*m0;-Hci9y~YCxPyl{ttf4QiL%D9hoL6;y zT)V)QqPPR8NeFSBPN%DpHLwH!Tq^1+pXt}FG0SRWXVg;yF90TP!QwSKd<>Be9Dc$% zGL}nnI5r#~Rhg-YH2Y=!fe+U?aA;t}8KBxAx5Z8K20sK4DIj9%Jy`5#@OwY6B{UWq z?4QVFZgSsem?z9=&b7n~eEL4k@% z?}<8&LsbTVXUn`eF6y8VGT(nI=HHcec;)apP-$xm9y(!qo-OTs+I2TpPP-F=@wS|& zVRpQ%EJ_pm>=a=f4uW@&{<0`Sbe*4sJUM%l1N}@&VEUxcr+lG@ZTFs^yD#Huu62RD zcimqedg?>q{PK^o+oN=1;DyBW181pVB&8AMl+nO)OOlh61 z8ci0WOg<&R2p>UEc`$_bjKJTHONGVv2y05RizmhWJ}_%ft0s6;mZCt}?9JCxcWmL@ zK=6xC@IExCn-Zq?aSO-L)6AUO(+S?R4!YvZ(+jl5FXdtNpvWUv-S%<*CuF;Ak9P5` z=1jE(QE|BHi7wEg&G}_fnh{Jg#mYv-0G18Qwo+O*X+@LePqf!U;V?1NqT=c+EHQvY z^Z3J$fIdnFhSoBnzGg+mp%PFlSchCjQNFtz26lE?s4CBWds=X{%bn(deXWKvdB((V zx7<ZgyiyuBPsE)%)U@>10i*FX9w)A~hsh6k2P zmnKuB&n-(rT;O{w4q7#4yVrFqIjQ2OIBMs_Ta)w^+6xfx^355Pu{J?z~c` z?nt3d33L*TXG*fx+4m%@5BI$f+pi<3lLeA&N}3$0c+I3WkLCjX#MbA9Z)ZoyW(a^N z%M@lgHC%DvT(%3zijI!B6VkjurvwH7YoIFlM2q2AD|r?s8KR_eZ<;V>qIsIvk_(QY z`ua5N>sLb=%l=e$Eoq9AQ#{k|3vGo&nihF^IB`a0*-4}#=o{d?^Co{X1GFA33v4y8 zllsrpdhKoGFvbOw!NdZh(_*kAsj-&$6wNHqNh$y@r;juI0kljYCqHeGf9_46ySNru zUOKr-pwbY4d)-vn_w=?^y0oYfLcFW8bW@g=N;n4udHe~qHR4F3997+C_yJ(`$Fv|R z+W<3BOXm}n1WQ#*!{LmvX)Y31JU0eg-J4CYC6JgzrATMU7RXlp0D^GqEhp|;;p5ES z;TeaquML0_Dc5b5t!US2kzncZy%f6!ab(^h;B4FlSv>F*QpedTCpPg@O4Z;fK-T!} z<1E^LYi*{j_OiQ%Ob6W1TIof;aa9--HFgz%vRIlW*Ij+sUE;ElO)s2SzBzosrcCYvA@1am@dnwI7@mB|utz2dq}6w@!Yc zg0@{?+oSk`IL{Jesq{w}2|6}snWDw(C)g!TAzUr6n8X>R=EICz|-4Srg<@B zROw`Bm-Bo!e@=c!_=B}KZJz;lLpTET42Bk(t?@cKc*kyg&?vWV2fwJFj`3g;!=?5Bk5JkreWVdKU zh6P6j7sDHr4nLY6(-d6x5!I{T_wNN8%mAH6E7&6)Xgx)EL}tt6;E!%(D@2F0CsWB03JtCfc?4- zRa_Ve=1^H9>~&%e@oVMVGoO)%C%J@!_9A(2nFtTU^&$Ek`WkDyvhE{)uKb!crYway z@lm~oOLXXcc&>MzF z4}9y=AF%bYB~~yjkNB)fXS~RlWp>M|R0pX2_T7j)TO$KbarUUBzfNhKH+g`_{Xp;X zXAzC_cRqK0+0ewz9x2kj@#kgmv4%}w3WLFLZ^pV*{^J26oDy^R&+c}IWpfOs-q zGCAQ5Yk5L5C#EvOMP^bWi4Tb!j@C+l_y%Q3xmiZs*|)5@*T21><=$?-`X$h+t__w&UyqIFkCr-QT>TC~QO zFx%QNd_t)+{wq4EVA7b7UZ5^G2?gbS3?3~Ef=y6Q9cs>nFbu*Nv3B1FaQMY1RDJLn z7VVGI@+%W_l;wVSx^{9`Npkyq5y&(+=w5wQg+E+NA)__l_zg&(H+A9qPH?}206s!O zC&8`*w8?}ng0d#P)i9Sxt{siih%KkmJ15aaBhD&Oj8jx&i? z^xNA=;P#-FH8HSX*7LIVi4t-#N;7M=ta|2-qVC9}%nW+`q1o)em}&|a>5sF!ViM1F z1ddp>h-}@J*N=U}^PBC}p^Sg%(lgYoK1fI*N%Y}pr=z!Bd%lyV5pv@Z{GJcP#}J}( zfEn{(i(HkjwV@rkG2gH;J#|Vr^-3#zqWs_injkh@1`yHhZ}iV8JzMuA(9Bo60s+0J zx@q23bh~6h&Vd&f@rmqXCtU@2PDVfd1OL}_C9*Q-TsUFT{N%ldf*09Tyk-lL8F{9r z$XlwMHbfyCX%iNQnNv2$Op6}NupZ9j=jLGP5cI@kSUh`gccQuL!*LEYF+%!qph&Q* zAj>|MQS8YsBcYT8!qoHi==JrvDed%+w~acYAAnnBQ$w?nnJ|Q9Tvi|JvA0u03UBF@ z=gdO@W%4Cq$om%QDbO1!HG}rE>IO0sG>TYycErI*ehXB_@6T1Gi?5r-Ckw#Ev$!;K zt!pD}c3<*8FjsO(LeOs)Z%)vnX|;rxw*ZD}F);nel?9#A*5WB2!#iv9>09%su;CW? z_d3_Xp{aCJ5M&h*(Gp)m5_ z@UkHFhl*0`^iY=_br@$GUlv*@m|XQLZrE4%kU0ggN!|Tj)gnPdfoA*GsPn+(u5`@A zHZY#NEEn!$u(c-LJw(R&Kgu+`;X%|i>cp-FptTh|?YYsZ71Ekj_Rr_v;i6&;bu>7+4 z{`46b(NI1HYUAYR>{?Uv-FpNyD-hzTk-A&I{}4=+M(!Z){2&=EpTJ+yE)d zfa(Yig%Z?o?t$L&NZ2LzJ~V$->0C@?Ns|*Y%V0h5%uPLFADMZAu@T1=yZA)QFCZBc zAPXj15Uc;z$UOqrq_$G4-nK*l_E*Aw=W(M9EtQNo{cZYQXS(x)mg7@urYj1JdtleRr{7kI**pTB9*pgx>b2_rJCr~hd@|~Dy?8o>pq!uQ@I3O#wlqf*##!L{W4+fu&bYIYLtp?(4 z7oj{i$GSVXf zM;iqzvu>d$YNiux0%>`_OE>3eWDw~F+i923p5XwxWHOkrP7m?(69<$xy*jAV6{D?r zO|y}c*~yE$0>^Hi20H_FSNGN_;zXR`-?c|zz|$`Qu1|tGJBn90{&dzE;V z&v4vY+~^x1*_w>1!UBQot5q;Wu>H35WF*ZDJS`3)hHjmC_T=egW4=M$amfl=I>nQw zMp+sG=97@+0*yk3MFR)_EzdXyN*4TwO2D-?I0Mc7i0kZL|D;9<^upgpJ(*+`XZ1_^ zQ;z9!cY=z;vdxuB^W8Nj2gDLAi1r{zGRh&7E4x=Rip=0>bv!#ld+SO=6St*ByB>Bq zgJwz#(HXo;X?>x);K`UV4>^jqR?fKO)z|9G3~=AGLxQX-HrpSAc|`%yIA2{uHMHyg z9cMmdeoApTe4u6B-n0nZWpUyG^$lGB8F31vizxQ6P$u27l%;3W32wke{i)%GIA&Uy zyf|>2o(rS{8*m-p$H;hlh};9Whe;mFSO(3v79T@F9VL(y*1tCF4TSs*^nrYpj z(&e>h`F^*}37orwH2)yy-nW#3p$j$s7#ty;Q3#&eYR;S0nuWXsMsg+~;5>8F;?3A9 z$%H@3Aj)1hR86(}4LSk`gDRyl1uK>e*NV*Eab^lE1%^=!Lr=V)PBLYoDsYoed>dHQZsUqcE~S4ZEB~41lO4ThWtu#KD}yQ382f z8GsM_40CSy;p48%xiOq5V>cak^R)VP#KSDXEb!;LzDe!*rG3~WBJX~DVMKgdsN*f688H^fr2oUgO@+mQOyx=gohb@WVj+y9~<|Tph zCx6cFJ2;@*1|FoId4Ej@?ET2cnWu&$>?A(%$^?ROloIF(#sO%(;RyXoW3avV#ozhd zPmD~#xUu!H1912b)=9IX>HS0OpATht(xnT=2*%)`RRR5Ode!TE^nSl@t+PcM#+ns8 z$`HeYU{IWu@ts((6$ZNnTCM^FSibrSyumb5ZsPv*U8XB8+JgD-hiR_%wmF&*CGZ;9 z{6u>xY#SOsIvI?aNf#r)FcX8*ylkc6d^lHp9O0E*m~X*5dr&(W_i*YC`(NMv}9A2_I>FxIK7@$5;lo9S&T=MQnPY7mEl?m zKow6RuL5hNeTG>#>|H_n1!s5(b}KeuSzt6HNSTCrb9!q=WOlDz8$5{Q{|Jq5F}{Sw zi!8KF3%2)jFjgr`>0l2Z`_`kZYpjn%#bhxYIBlE;j)kg!5Lp1xoQG!%=BT+@vF_cm zlSo9|nFIpMn@lI(C{WjMuVx95$^b(fu+LS9B#R+e*#QuL64wP7QrHXp$b0|}i$bB? zS;|<~WQXE2sjGpgbI3L#)!QXrpHKPidnq9wp&=3{eJbg9HQ}p~c={ z)$a1-illG0AT^u&b85#lQH;=z^w-z5#vL|m*Mm#}voh0vt#-#JTI-tZ)-fiZ&NVzb z{2RBmjm{)wg|c_FXrr%ORs$KR9&p+LjW?^9Y=lHZ%EWT=y@P7h?hKGCO=-C%fXRrd z)+*WcOp|yoR-Z@aVu4;WYYG|Z1+s760jN*)VosAd_pY8D>;OMq`|tW|2XGt<(%9Du zC^O?pObYc4Uziv*N(GeKgKzU}^!CS#IsL%7lY8peH6;r-W!it>UcI5WosYx zT%{_r5AO$6qi3=c#BT=YzNP;*kM7+)5ClT$HH5Z2ro93Nt^;U*>i`%#?1Tgd(`2h5ZKo9K$@LL_BFfb30!_c16wlSU< zFpl_;BcA$-rT-thV=0KA#{pUOI{35ABk; zpbI*b#@OX3#^>o|DRXk$D4p-dD8abt+_Epx;(Fe-%t1CkFhB;gKHlCs7`Sv&#Jr(k zN#~cUeOOTB(?O%sMP%`C5S#Ct#jo4}n_9QevKX;yrC8DLJ!J(>Nq5(*Fs^O)>SXYP zn-*h213a-KOab-mxVgIFTlP63>TcOZOCY*_VkB#@HnIyrTGQH^=xU;Ov+Q9FEPi4+$E7|Ik(h}hy7Y}HStZf9 z7q}Ywp&+FHiX9l)&X3`Y$vrN<5J}j5gC+TgTT&S#kO24=?#+e=_FKN;z^QM(v1;jwC_5&AyNsN2m$y6_Q#XL zUxv{`swhv6j{F>QgAvMu|Me~7TY+2%?KhXHO{7QEO(ZQ9S0jz-3fR@fO~`H+_XJZ1 z_xZt+x2f6n7*FkZvFpHPzLka}7Ow3?&D<;{_NBKO0AmEbEqDqHkzfT{uE|bZpA|A5 zf^mX{W&qT#Wj<{qw@U)E*f!?6ccSFmBK_X6*L-2z%v%L?NY9QwIE1_bMJ5@=@MlP( znQn81I+;TRPAM^AL~LB=e!s{d7{9PxzC+uii);+|^wb@wWbKy%?n+6~ zg@Qvmi05?Wo;np=Ag=iqEgmzm!9W-!!Vv9_dNONs(_EGxK~BQ}5j{4ri1NV4fvc=* zMl>tR((*KK`zdfw0CvlHSGcM(@6%0Z7i$_c{>qcdmqbYxW{Hq~k*$to*8vVh%Dr{a z1=E3>M=@j#y->(9+S41*Ts&Fyr9nGOZ_M8L^!DIu_xBa0iVH#q+IwWg{qb-25ZV!s zC))|7f5rRQ58{Y!H^;s(5(u_YDkW@I9eQ4{pjrtDd(lK%oY;@BbZNf?8v$M*87xi} z7ewY=C=MYSKNey!+TD9{f~GXVYl>G5QN$r1Jt>5NNp3(quNC0e`i z$oMAG^>xd5F{J(SCvchpPC&5%x3Cm7=RvV4g7#!vxWedw#{2-6)$Rl9X%Z06dXqJ! z`sV7OhWaO>?r_tP8yN6svzHwJL)gc9-mQ1-zm)%pqxdUbz*Jv06jt38C)BvRE5ul9-KoBWnV2m8pkTNBVq^Dht9!gmC%aZu&%7AR zY<$(}-50F=Bs&RUc^bWSVz?G%!*>V3m?dzWkf?F$56{02?ctSuz2U8k)xJZTS7uBs zyG>{r?T2L2lz6~>a#gRi!_$x90#t`Gl3AkcjGWh*UzZ2@4djnW+0bu3zY}=Ij{lB@ zW7EF#vMq1&_zsD3b{Tb$o;%j8zpHPYs*Q415A_hw4*zq==+Q_}2c2MiQ6VP>l z+R;Ni+R^h!nOW)Bxet<*t;nK;d!H?6pJ0iC^wT<2e=0|M7GHIK`m|E) zF&fw7^w2~!^r+X?<}B}ccg#h{A0p>3D*EmJg^>usdv66_U7Cem(z}00u($MwbZh#s zbeEap8P3!{{GvK1H z?T?c!Q4A4RX(3IAfT|siw=VG#k$2;zoQOmEFHAUG6;(}KJC6~%pcQ;1{76^6!@^?E4p6@=a{ix+I*cAU+T?mCh50hU}u6wZn^kW8w0a~|^v z+s`q*e+9C}f`Gh%eh<5HM@NSz$!HS!{fD0B*H?;>0yvbAk(c8=yd1ZV$J~<0 z@a8n^jEw=?O3PSoy)lJ^tr0!=6JRIaXIcKB5@5i!BbsCpl};Hd{IY-AYA>WpyV9t# z&^I03x~(f@y$EoBXeCZT^~zQPj9ASsFHRk^>~hm!+=>d)|+=kosg{e*SSNtJ)1 z{+YI8oShd;3t2m^nyLT!0jK_*=7|Pyr&-bO)bOwAmtCFC)5}lnIvri5goGpG(MJlu zJlu);b>)X_TI+P+o==ygPFI~Edzwq|-ODpc&1+%fmqZW6cF+ud1kK5Kt{>inMGt@Z zq9$Y1bB*u|ee5qt%Gq+q0? z&>X}rRI$bs)7uZQCgjDfr!s7-hjA)6qs^K-9ZH|JFm9KY9v7&5;py*uY3|Wf2NzFm z{%2TPhU`s~_B}l+%D^)jU3JiJ*E=Vt?6FR?-519M;%y)Y~0%j;-5^%)3u|MH}>mM(s|Ljj*vf}fOt^W93WF4yh zY~y)QYR@?i1A=T;G(tW%Q&{oVei}~s*fN5Tst(k6PEjAzz~_`WVYyV8kD793-~7CZ z{1I@-G*Joqg1G%mr&G_&gM0JCFE?7+4~x#wMQ(m?x}PhrP-aAYLQe*}_!3pnEWPpi z{mT~+D{sdF>WnD|u>E(UEY=?lE6S9HOFpoYXt3-S=*FTH@rQWyQdqYh&mk0_gwyPU ztE}J0vx}1pE7dnYuYMg?d>%)4sC%b(>Y{Y)5cq&-1o*8+cxu$!IYH&&38ZY7|58XR zWN8R_i#{c+cz=q}RJqXzBpF-)jrUO8VbK}ZGyaNG5K3K6xqBY%X1z4oVl}LYjSPU< z5f-OS>P+f+h1;0_XdvC)rL`eJbG-VF(I}qo-Xg0qut%Ob}NWnkz;v=<=Ie?%U zFZdghvfOdE?E;r0w!ug=}XsKa0q1r*HOpz^FLsJ5SnP$=1x06lT(iVfs~c;O?ziXlVa4;h#i_(gCm3h_eIn^ zZ(7M0LV-WFCL^lDE^*Z9@ev`HMR4)=N6@zy#EP&YwC5SQ0AKxINEw-H_f8ox{20E# z3Uv!A!!kl|qYrq|Y!nn%*e4@^nXq48^q=47ZN_m4rcN{jt0Dr@as?aslVVm<_|mxV zoeE_{xmGfXGIC#R1#0pHDt7vD;8QCRc-PXMKkvpa4D)p**S%Kbc<)8%Uw`)s(wee%Qx7DbUmQ z*tLWmYZ|Kn?m&TJ`}ZH@PClhl@gYv^PP1>ltV!ExSPBa5c8S5p2&lJZCGZd>9HNHx zOwOmri**yC0r96_5SE{96o)1x=zPenwu0}m^?!S9*5=Emda9x{p-d8NEfxFe!;yPf zF4@ZBYgzdpn@CjJD1b!0AaFk05S!=aQ%n%Ff*)fES+Yv&>Y6f5TYU^Q?=Acy5{Bsi ztY->&5Z7E#FYBSH=!f`x;-OfV)_S_OmPS8sn&!mSmQI!qwh&x-+R_DR_U-2Vz#}~K znb0`x$Bk!&n|x`*4pFUuU~;^eEkU-n_5>w^9FMj<3|4PT`SOUR#r66dChS=bIK+qrv1*lu zb>P^63FR&z_!N(WeGkXB6Q+nNs7&-;Q?Zzk%L*b*UeS+b@J^iA^AHu7Fhtw!dTeNN zf}JA=4BVpK1-mZ@!RjkFZiXhHZLa-7N+HL%GzJeR(nb9R(Gvb=PBn3R59|HtL%eZa zG~?Dbp-3wabBaTv1RSOvWt-P7xFQszK!&6gSG~0XYJu17`!$^-qfameK{$hgQ&;`0 zXUs8W%xI;pSJu4OGc)l~mdq39PKLb-E>9baxEbZ>Nd;Tey>1mmv^*X)<5PxOt^5U* z7WMy7V#w~#igH_Y1UtXaY4&ue6QAZ9Wc5lvll4MH^`*Czyvz|gyEzc=k?@x`0cG2I z`;)4}%+cwBaZv5ZtLVHl@P=E>BOp8{l%3F(Qjk`d~=py4_1pyH@CA#=hK{Z2A3-G)6 zm^B}KXur^Sq$#*FE&&8~9kH50J!8ikl1WpM<_Q{P zc5%;)ch`Ru@icetI_Q7a%U{Dz!{l6T=LNls9}?&eJLyc_yA@W#RiXVOdZvTfiOsp; z05b6}q&m%TSVZ%cU_WP4Qk}8S(b#%II8AF_Xk`sLdbl!-`{L{g9|HskHx6FHzc{WXD-WB7R8Cl$js5kS{=asCb<6399izwa94Ik zzn@5%t~3qC;oR%PP|Z+2A>jz~o{J5xp~<0f_?l{Pq!2GjGBGt7_^0{mvwne%N1=%t zJ<#c1U8)Y!MloHG1y>(xM%v{O(hCQ09Fed_wpSp4BZ9?L`I7>e&S&S$VSd-;;y1dU zspu7PA(gch-kAPtwb+g=bQHg4AkX@4V(I??v-FgN7(df7Y2xNfV}GV2Fl`l` zai(PcBtsM={S*+;!?|D$45W>F_2otAe8NCmX1*o+UWcA3>NAULjIz}VPyiK~6vK*O zKei5tSd}EWEn0%Co${O-N~k)u_i4ysK3OzUQm0!lfAW}VX5#s8NM=R6$^<-`1PR=-QrQYS^I9{O& zkbU^ldL2IhN&R~D@$-jlEAPi%fbB?t(M4qHEA6^6sfzZB1q8KFFog99ETW=5;tFKw zS#bc|)FiQO%-lg=;2@XRr*g;h5Mh}43^kZ4W*~6ML($JXYvB!RVYN!n_LMPq7>mpanj5N2jbQ0$rfbl#(cyJuZ z*~p}cR1%JyE@E2h{tJ$T=HE2yI$MvNM{H}|IOCaLf1v;V{_(T3C%Baz`uHJN>vO&< z#}9pb_ezjIR*8k>c;MTOJXZPcuWwQ$mrvnT!=lG#gk&yH#7a~XAs zxQo3qxZddfN7CHhC0sZl^8)D$LGPpRLCKp`xF@{AMHfd7C)xS); zKnb)2-woOuzNH&YnZfO58i~sVvP!G#I{sX5jacBi{%C9=_BBH1-r7|q(y)8g)O+yg z`>25UjjkW|&_8!Rh293U?3w9=8IAD$4C+YwybSib)IGV~k%b;^TLTkyv%tuz z`MT`8KBV?3$E5KVxl5Lj!*O7rpM`}>Cm9BG5rJv9fg`R+ZHjjwWBQi-JYhCOup)Nu zV!AbC?=Z?({S`-lF0-?hCTB^oyAR_efDaG_E+4Aop$;OEGL|y%l;{UMb@HhEQ`+RV z>U`OfE(!g~pCjqB&J0V+(m0^2>;EF_&Eui|z6ao8ieyWhZHDY2vep<|WlzW!B}*R( zSsMG!OpB!$yAUdCC{eP@^id{?8WLrjvSi6l5#>4W>GS+lH_ndpr z-Dc#noq$>J^go&C1-UQGaT$*S=R%`#3XR)=bD%ka;;3t2Rn+cB6eQ&$d8@6Rm{T%v z5eSY^9!$3*wh{wq(wZwOvvSgKJ=PiSgtlXF`Ks%uo#rRwcm9?`4)WwH-DY0S7gG_d zWsXVhutY?IEu$lsB0_Xt-wH#)xl-?&aMzSV5*xm2z-r~DS*e20mod@ev)!G0pMRZs z)UJTs-Kad_qM7wi$KO8ct9*zEX&PeR5YE^P}llsCgE))9=S&PNhv9@~>La6Z6LXY}yLs;-w?EcSK=VZ_u1KQ^*p z>f~+KGY(670L*6j=AeRsQ4 zGQu~>N#dO6!w9P))vRax=;{;=i?hhd#dNemn&e&P?*h5zS*jFk8c(m=(qZN(Ug)Nt zYo*156)`H)-FVT%Ly@CNJO5DoT>eoQA;w#h82RqyAdu3t!dUZUcH#(JA*zN;6gCD1 zhAA?s9Z%s%`pwBS#b72}s@48br&^UrAtizP?JcaE#{T*)LK5kHz_^0;E2z1fUGJ-# z?pjsE0*uxb?RS|?Z+7t|-$g|oliAvj747|33inUWKJ4lHxAM_tv@tabC*(CU{4htM z9>${g=RP&bpugKPNcz`0Q~8Q-zV2f29jf){B;{hIb_Stt{%UMmy zqiS?r3$<%jN8lu{>Jsm)yz2Ts%3Xa|s|f?jDvih5ZHcS8w)kxv3>X53yZxcUH=)89UqIc+!*#jq1+|EJf=VI zeRQUIS;=!vNAdTRMx2+pinC9dL<$p*`A0i^+)0n0+yRL-@pM;Wf%{{}gdYbMSQzIY zyEDS|ir7v5?4$e8_tB*HojoQan=i)lxV?qxA#)()=K>#XBQNO&8Fjz(SUQY$ME~HF zsbjO%m6^Hvyrpm!p!3d&9o4^mhUso1`N7M5IPaCATGA-O)oV!|@8Ei5BmPg9AZgif z+?L8&+5)`wIp=@OrAsW`gvN8NZ%l`HgU0{4LYNuyiansT;t8i@w6~MDJs+S2ooC1B z$nXZO{1)#_LR0Hh*Nx0|y5h$p`{4!s#T7z{a5NTwUPs1o)nR7ma9 zX8mp;!<{fSV=SP^&5xDh=AHd4<5YpBW~zo9g>3}nq(2vbBefchlR@26@U$^8;$h_m z?kh)8#)jh^JQ12k*hMz-BA%a`AaDP@juvO^GdNkEKnbV9T@Y)Fdd{_{JQ2^Nh^?5T zh3#V$MF&?{WyUVhl^LXHCc&}p+x{}8o0k69#f@@}$1l^%&PJv;5+e-9fAH)z+6k!0 zn?x!vs3Riu!9zj43ghTCQ_hYg2O*{Brbrmz*!r#nM+!nbuWP2@Cn$ z#60xyNf|Tb+R6v@(m|b#hK?HV{tdn)Ll=`;^OUwulJke`&C8%7@i))!$DSsQQ^+)) z&mC-qN<%^M5=Ldzk9|}v_NH8`jHHfR$E{P^UXz?xOeuDf4ZecN)hISXR-?CIo0eO8WhVYYMo0QU|XB!vi!*&~+z zlO`k?>ga#7aM72}S0p*l{(WDrc^s?Z_{Yguk~+Fy6Mw>PEvV_fQA**Iv5S}g)xt$T zI-eW}6xk+&Y?>$sU4^$pyf zno!gY$5|2OFtULA4)I-fBu7Nsl#nYq|5Rqf_5DAS9A01G1b#ZA=i;UBFgSf`pJhW8 z&)N(E5qGeQ|Meq{SyoyYSPLN0my4zztp2vINe*AI+fh8-0Cc{*y|G6~zAVy~^ANJm z7^O&(&{VfW+ag8q`MdANbMe)&FV6cqdNNOhO*cZb3Y17Fs8Yb++dU$cE{VXQ!HZMF z>)e5bD+nPgTwDB?A{;ydC&n3{k(_@F!`cz&fm{xaW!iA_CmyjIJKg||-Eiyot$x_( z%j2LF{oBGd5J)yJAxDjv2%M#Ze-TE!luwIO3EqQWr9L|cg!mItb=dVoH8>#NHjzsioZPV}VX-JJ0b{slAgzV4ta~z7B#`+k{R+@8%}AE{ zV$iivgh0=XlYEE%5!>~E@(8JV0W+*0M3#zw6iYRP7(C;AvGw15&!IO(F8pm@dg66O zOpFQ=3@s0C9(>KM(i0m0L87In|OsV;mhELtZlnQkZ?rbe&^$^?6>9y++|9AdV zimmxqUmo<*F}}KYo8PAA=ioT^?8iTI&w5le%#W_IM=t;C`7kwL=nbXHL+d>Decb#R zR+aBd)2Js}t^Upd+fbp-4$E{*A%^czJU7Jf5#B7&xbJ*CD$)ACh4M7g~B9PmS%Pv`*^45l#Ag5jiRSsP49r= zb4kvtTDI8tB*AjD!0P%mkvx=qp`j7aj6#Jv(QOk#G>){-lq(AaIYv*tV+dx{nXrwLtt+)>{=!!;O`!Aj4J;|LDL2c8&^$5!uP=`qaS5tw?VmHCWJVW z>$U;ySZ-mNl{HLDC%nQGC5Tq)Bm0mgl z$3{O|v#Lz?^fKj!sI~K!gQ5^SIWPm4=(c$#M&9LQw+@ZG?6C#Vv-5W^gf<}v_rIkq zj+_}D3APN-Zxwp2pm`Qc%QP-WsRemleJKQSfl_!#sERRhG#{&_UxTQWyaYcEfNS_}T3a^J#hREoeI_?A-j zNxcj$@PiONwKyGn>FuasO3vwATJmEnxLAQw2O)q@G`cMJ&-@E&IhcOoR|JDgek^2h zPzEWrVqi22`PI^lbXZqk85*$ZRU|ybDgN32W+^=7E*L~-p+v)_n(27cO?+$heFS1# ztnjoA&g^VnnDVK2V3}TqbmaM22f&1tp)BNmEaY*YyeGE2)~xRG4a(6(3h%cSMWj%5bDf3iee&F{8J0Qz}(CwGtvf2OJXlW;}<1Jg zOpzci1p?&>lI7f0&NcZm0f^6#Dn=8K=X>5X5%8xOag}#Xz6@fZ@=Tc!?Bt(y#EMmA z>>;S$;Oz^c4;UCUK4&F|XdD#sI-en>e5&OU+3ocPNZOYHD2%)iRW?r&Q>($>yP*A; z%dJgAXjh3<*uR!W-hYX1GLNPXfcV$k$KMli75~(T&qrHEPZc4y(5a6PXPd#<%fz|( zr)|mW)HU}_YKGB*=K zU!Y9-Y5>Cb@cC@Z!Gx4iP?d$|zeDolY^xoCo-ZGl92$w{=ov-q8T9g_+lDx;bOIa= zpdcc^$;Ns_2yNrFSjp*EuB`mJ;s~lB`gU}9ak=u%58o6!1`}Sb@xoU~nY4kjTpRwj z#oEg+O=y?DNAK#C@zN_dKmGx2_oXo-#D2Lt^uYF4`!$n{-pka*6}ne@SH|7>=UI2Y z1@oIjnhQHKMvhMC3@>ijzu3Bnee0T_`21|tSYw`2*E9cPd)}StW$otajHu1{Z8wDw zTkt6t#?g>fW%ZogMoWoU_CiSiVxBP~`2$%2^fgcXJ?XnKL*#d@66dW+-D5|q{_Dc_+ALrW5)cL!-%)yW`Y_<>57BBeh`@3!)6FywquS;?pJ^qMAe8kVnC53BS z3&G#J(K`2{tRcJ0ZEj`?fN=^_=E9f~;(au+?_Vmbej6FDYO=N-`3vZs#hA1`7-!sz zssz^_rcO6%iEtTB&*Y{vXHw!a<-(m10 zCxnIEI#*Q|ssNBZ?q-t11e)3Oz5)O$N>Hd{e#;yfYqMf>_AeLwJ+_jgZAwsW{FomL z*T)7J@L!5z*PUnIBc@=M>^tuCg+Fb4up9v?*8>q5so4q72oW$RYD~CphQwV-kMJnYBiDn)y8Yl>?S-$2EKN2ucIWWT`e68pL4{RWB z2kL0YP+3DBV!Oz#Fnz^?he6LK*3~s?Za49O5naw4kLlerMa;HGfrmfC;OP;I4rSwSec3bRu; zz^*A|@w1T43enlVdM?cf!Z~y>_w8>L2)GxDZC3y)Pn|?fxN(!LEu^LoxcLMw-iTiel-ua*0sD6hlmpT>4})N^ z=}^YdiUfiKP|&oS$)r9HR=z#T@Iw?>90GjP!C)Tpi|K!z^ckI|v>j1gdk%pSX`Nq5g0Rl{5{eeXliPK zpHpWv2pSoHU3JE9?QWi*&syi={7+auE<(uhunQf-s$cu22;sD{0hB9KM3(R&*RWa!ogH8a+Lu0CpZ_F}dIezNggwzJ<;%^EAK`i~nu#Oc z?-1cTHbZLVZl7(=<*Iv(4dgD_liiStK(@qz$%!F3=O%*e`|53n-GnA#?aKNO4$Mq& zlym$5VkQsFx4@ss>T=xsy!EbV+GRp)?tud$r-f$A!!E;9*Ut10=C`Y~;`@jff@LP*5dk%m^tYvFa~x-aM&Ef(c`Hkgm<< z&|mMtO|xNuD!OkuF}y=)mfg6eZRF5lI&>5L$D2;LS}?pZ$AoU0hVFY9AMU_-N)av& zp2z;nMt0r>oF>NZ!u6ZdUxy%w@u2^2xMBJODZ*G?o-gL_a3|t^)6?@lC+<6?U7ah0Ksg~9M!;}n+Tz@*ZC)L&$s`GW?Rfc zF5RjyWi}vF%*_xQWt56gfX?uPtjy{*kRvA^Tz`ikM$MG)VGlwZJ-U;u6wWsXp#^+@ zi}KdA4_WGf$y?n<#z_6;6@qu2 zskxa-hZ@4vzu%x^sH`*VSG$H!{oZ0n_EyD_$Ac%4=H%rdAaB=SNl*lm9nK%VJ%l#R zp_spGl>AFZ+zaz7)2`!wJpY?v)wmp**~l1D!En$2kxPOwHV7j4n(aSCPyTc&A1I6c zAiPm>@8HMvQy}_pK7asS%^QQ-}#v$N;nu};#EsPyJ0PsmMR4Y>u2eR?g{wI+M^pY7w zWEYPgItBy+L?o;SzO%7HRPgV2pl@m~fN#Jb1SqmLqE9zZ=KUwFh(-tZX*%5l{_$kp!kGD@FD<4uZwzD$Tt||;I0rpgkg``fI@?DXvrdtYPSRc=T*}YDa_S- zIrF!noz$wD9x5czi4cg22+FaRDEJexk#^99gldx}u+HsA=zZ+*0q<$-r~_p5-a(x>q}VH%rp|X3M>i+cF}x=J@^2 z>UYI96~e*+4$K?y#Be7O1r32ErlknhdOa8ZD}?w(65`q<^@Y&a48V(l#lv-Q2P(HNhe!D>I`^I8-&T^*HB{z?XxFe+k8;gLln?8$`#FR~Zzk7Q$_ogh!YX4*d*; zaj4x0TFB}mh?()b>Rb=*(YfrBH&b9V4wNq=E6gu|3R{BC#xuY=i-1+k0IPlgT!yAr z7(fufTKM?lmps5*mkJ9zV{&9E`;!Sv+5PO2pklGEx>0Z{3>a%o`8=g+pe4i9twWmZ zLI$5iGY?s(p8Vp%NQFZ&-FYL{9QVfG&Att8*FFCB*r0rg8T%DZ_EVcDJe>chv!?*@ zh7Xi%P!>yI0ZlAK3*Z3JRsa9F2jJ6dN#Qco;YSxbh!LkUVc}j5-F6NKVGU%NIPwt2 z1pNMgzip%b@3#*z9(h715q04oLiQIQgGwi+!tr0@kx^GO%0iVGYqq~I2(s-+ec6&) zmqg%3m`k|eD@1wvAAl<*jQBOhPX=cJdou>J5CdQh_?6T>7ijbLJS))K`0=J}tjXchQ8LcY@Hc>z@u zQDWgc z*HFQe50}D^(GV8+8V5e#VUWYHsLq}+29Ykn&#;~s1#UE2uU^I%DE5I>aT2RhB5c-_ z%@v6W_+E9!R+Pps*E4nxd;@&Az>TBj(}DPc`-vYknpMFsCpqUlE=GKr*8Vn%&%dha ztIdzfgp3VToalVtEINCX8C$a^IZn(`kYdSoUcJHCU4G19 zA*)*lXk%&b_o$ljcJpv<9>l&MWccG0+W~%DxJ+OgyY;FSKA&{=Ot0WnY@616Y8!-d z2Lf5hds;wp1LUxTiP;}lta{D5`4gH8%Cxe`AUkcJK2sLU0Z?Jsjeg1Qu&n^99&@%6 z#6@O-bYwwv;CZ3a$J-3Raj>H#JeqL#h%y!n*9HK3vr7yZkn`DXm`~sA;N$K1_RsBs z*{!p~as0CkEq03c;>0xmZYiHWgD;Tov-*fk;>eI&XHO404*tv62lN9Y82Wr-$`=s} z+mZdOWI)bD?L`$?7&cbwHl{uRma!cMl#Xu{6ywIwpG(%iC30zl?1-&!iukW-R@KZ5 z0v5rF0NrYKi7bOh_Rtt>8WaCXo0{DVVmBTZvIYtpCZ^Ahklx-e8G|xD?V!2pE6KcZ1$(Hgo`1Jf9P6{ zrWzxAz^}H{pdKc{g-3{lRSKb5J(jts_~)ry{lqt_X77r`K(Ychh5w80lwU%g-FZi^ zBB+QY#@^RYIx9L%YSCTiBvSaS?w$2-db0bv=o2T>X^VeuT+QdHZqq0@ z@xg?or_bm7`Gt;9_xFGHq;}T1&S!o52 zh_QAMKqT0v{Io)BWLO2PBkbhyMRm+Ag;PP2>jnY(tPuK!rtfNK);9!*8tZsNV3Cpm zF+@f*EN?1^&Q~0hy4eg-NhI{P$wh^Tp07J2fa>}Si#wxn8vFjis8V7K1VO)k6hCYY zLA(67qmlnb(<11E;wKue5NErP`6mTp!|`00%&}|#*Sa8)KJHWe1lMJCBwAQ-JBrv> zIn2s+7?(k1C%;;(?@#dYomlkx`}tHD0!+&0BLF7GRVc9(f<6kGNf+%Pjw)Y@tWbew zBWUW)4TuH@h61$V@`29b2O*!t05_6_JT^B?j9$mW$m$qD=N5?RBZQf8r5nzbak*vk zVs1Ip`*FNBh^c1$YP2*z9jlK`4hi=N(A^U~Rf@Cxbdc&IRLh{~j57e_nzGpd0p#a^ zg!V(O$=4<2h#^{q2=anD=8fMafFm+xY<)V`v`Kb3*n$BHXgU%iR?}Rr3u27hE?#@a z)qm_JBQ!b7fsut({i$P$CFC@GBLa>YbiL7dUc}HhL9{^%N6&n!#+h82f~(9eKj#$4 z`QWRh9YF0U5U+4=Jqf#*PGu z%hE|XZ_7X_hvaM!gd{Ytza3TV<>Aaq-F$}6S9W}@F=xs60Jio8k05QJUW>oS-@5Wo z3Z&uiepV@F4A|6_2;bc*xZHA-(_byArYHPde>-}dL60~5xIZHbL%Cy9)A7-{86g^V z_1ZGZ!K5as=`kWyq^3r;jhy_rLmO zn!*cgdK#5-m_blj2m-w!_cU5_5y8`~O+hnnj0z%GC1{jL_K^KMIJp`DMoSH+s!f(aA&BFs5<>+js zQ)c4zNS@Nh8k7PK$79MHA3^tTc+0am?IY(#f$TRBdc9XhQ>Q^{pQ*JsmWx3@Zt;*x z=;!!O!y8F35gj1ITC3X54w)yv;wQllJ|A%6?KX@Yunh~jm!qf0$onxQG1a=P_Rnn? zep@_SowecDL9vZs7 z)BY4-+OF~I4h#5{HKlmE>II=%3AJia(RY$+;;k!I0fvyz^8npa#;<-8ztW}f`zap$ z>Ci9RshE#Ho4}gUHcixz zc!Vd0x0S*Idk7@;cTVY{k*O@|QAXD}L*J+X{ckyCV31OPbnhrtieR7JKBU8$fi-bQ zhM;})EaM6`$)*~lV9axAb<`A0)`ed-4Bl9#j7UIIuE_7Oq?&Z=${R-h{su3m@mn#Z zcbgR3Lm-LBUwr%PK59A15*&G31vGUItj4`PkZ@C({w$t&#(-`Gahv_^l&Y`^@d400 zI#vJnh0Hv&rG#mY6mgriG6eLt|JB=$Ku&g37O$|&?HzdZ=);TbhK~;vic(&+_kZ^< z`S*G$ai`DKj+M(#>$%QsbL5jb94&*^_ql1SvBvz!*5Qcm&FYRDwtJFa70(WO@CF$2 z@mTKJ`$4He+f-zVSo3aq@aKB&#`%$VjwU+u8+VFVLgpMF&jeuz@dkcxx1uNGnNKkf zW3e<#8>W_6@~#agN94pMyg))>qOPgY-3&xaq-i|Omc0X{@&e^;loP&7y6^x@ipp^= zTw8fr3+Eu5)h1perRIx;PVd%uMce0^)HCz<9E$k_(fOZ>M6QD`9d%h(EUdS<7kO=vm819*aM(t`nhPh{<{>j{d&=~19C@&FT2XC1voW4e*a+9Zw72Uu(IannJ~)Ze?6iA56@i??GqSj?M(0Z_i{z-2#uprL1>&Ev9$^ZX!t0nVoBM{E zFiseK0OGUEu+x~fugm`2&CwfHfL>8aB>^Ve|DFxwg+U%Rl7#gP_WrZKQXHROTlPzc zPM+k@gz_^puicSL0065h3{zSb^i`UCBv;S$55JI5kdJ@Q1r@V1S$|m=5GTyVI{z84 zIfODLlP4dQ4acJ!EAT>fIFN$E*=~Mvs*fwzIEOZmjwM-l%&4N9W_(>TT3KELQRgxN6<2&C-?F_5SHO9a96w zNxTb3Q3mtB7Ed@St6_!v#oS|I2C9IQfxqtY@UK*YJ)7q(@_XfM{ipu^fe1FaJ?D)> zMz=OT@vAthAC@Tj&0HqVnT#f1crZOn0SVl$M$U2AjiuRHH=}K#FuSWNcbzJNglYSf z{r8Xyztkv0WBzW&dMW4VMCV{A^L0A&rylFdIqSz%Zkl~fY>qb>B&l?dFP`hc&o3?K zb^Tmk&2(9q0Va8mnYH7|2Vc97sc7T#zjbYk0@KxNdYhDHFJ(0HyL~NkVuIdjYqSVIamRIBFtYztq3{ocP_o>F_R-Y(wX_OFGuB# z9J0Wvw89z?h2pZvU2D|I_^YR0*<)I*dlQ*mp~lkVGs!({h|oO)L!rX{hg+CZ-nsp` zk*O;O{h9c)w@lQ92t59t!kgFbXZ&>^Td&?GhUF_)8>Es1+uh9(n#1){CZx@9fcuOkDgu~rNR)5w zQv$yDdqqFfRJgcspHKI4qicZ$6Bu8-0zSX5EL0siJ~^A(O5_~#KaF6xC!=S8I72d$ zYgyh+ckm58{$^g*T^Nk(yIwFQS?DHNeE!d}Ujj~+{?R|$=b3|&da990+vG|t-7;?K ztL^STWh%vT#4vQfmzn09jGh+sVRF~O*VicFV1Q^M%^5w$q)tjCoF|qFVtQOq7KKz$ z@6J?7Jl1k!F8rO3+CpU6=T|cp2EkVg?C-{M9efKV_4N%_9IT_b>s0VM8=i`UMw>cu zBe5cW@4-_+(Sj&f^v|Ro5ikS-fTJ}!QP*Jaght|6?bmR+Hq@Pemxu8^l>m1*+#mbm z$(QP?qWn9Q*U(PvYRz~!n~Np~TCWx$C6jjgDL@TB-FekM;xN=$Ry@lwIGqHgirm#y z9UnKS4F-2(=<>e5cB$1fAKN)bEQpXLQ47R&E;IRN3t?$t))9vnrl9{js*vkxnp{+h`k-Z&qSkrKvM`rs;%JBFb9U^;61M+4Wrmn1ujJvi;Gqqf_^>03p0rnP^- zN|h2i3C@~?)BYXXOqkB3^q7pPh~f2RZlKAy+j^{f-PF{u7%Z$Wlpm8j5G1?VyEe3Ht7>u7hv6oS3YCAM-cl@jb_l0U&+W_Y*X% ze;a~SmVWa+cXC<0X1CUz4-}=cU$+4};gG9hA1{Z0)g36Nr}Q0Z-V=NXe`0j$B;9Uo z1#0OTStO48i&WlJ0{MuWP@!IhD_e;t*KZXz`r>(Qd2-F~-LP8KHZa&LQZi0~GtL6F zUX*{ze*Ize98-y#_|VopnRXx%b)}%J1%nk`jg!KCjkr2-824;u6xN~o-ThEO$R|vM za99m11R^{gm-B=5C-XE5_lLaMJ3-io5>U9Aeh^&*uw28QDOr&qo9jB$o{ zFwa0WHj#HsbK)gX;|gBkJZ1W>Aml`!Y=I^I@(}{BT5gMnc3hzkOp3j&&${>Wb~P+q zCQ0fc!iws4qWJwFzZUXVgNmqcHtQh!p?K4&x^~KTN~mu(c2G1yoE2k__&uwz>Vyh) z4jDYVUlz3!9u&CGc84RJoFk9ipb9u|+M=U8m??Z-=_Hw{CaH%;6X|~7L!rs#eU0@I zQtON(AJn&w5?s3jMN40hWRXd9nm#l z%O(%dQzm2o4d+;ccUM@x?sXDV(>w~Geh^X1W!*$~{yZyiSDLbK%zvh0`=gW}|875G zpW0JOnFjLAZgViTzH*aGTHllvZ3o3ap$O+sbX3z!oLWkug=)#K3aV`BwI305-Lg;* z5LcHx`efcS3Yx~VJ$u{veOPmFTQVo^+|7krr?DZFh_MwZgr5gG-Jd7m2Kl15t2nxv zf#GR*%Vf}JwK|F{csKCbIpA0v1Z~-dcy*LhtlnZ$ygyG`xclNU{oCN(ydo+W>P%|S zvoZf;@EV_tyy+xmf2w_m^bdHr$ zWwlxZ3e$}muhy>I8SLtR)0VBN&oVUydX|^fqHVzPOdoL4^Vjt$3&k!3ftuB=s~FRU z8Mi8vCWOBpp%V&ZGteXACH0`wDy*2Hi6=Ci@$0GAld)f|RzFc9kUm+I+wJX48R7{I zIplk1C4U*bkoY`Aqnz=@AHnP19NU*(@y2~0Txu&gy|oRaXT8DKb7XuaJo^eb*d=ZU zkD#T@d$kb}OfWY!cK`7*|JH2zM%zjL7$h*xnyxak zp8}T#FH4QzCf=t&ab@>drZm}8VR~>++VE)o>Z{q49eIKR-qkgSQQF(uz82rovrs?x z!hE~8Gw}kAUv-tYqpB*3%vFl5O{Ub-_`6T@q6|`(VrU|wqTM%q-^_8QX{HyRb6|RY zykk$kj&8P|EUxT}|FK%5m$v)Y-2HXsiZb7wvn<;W$;1S zVdhaEL6D8^Lonw)@1xB4Z}=`7qxIp**v~$89Cf_?@O+QJ<(&&&7D8|vQdE>UphV$A zsu)kH9%W)pUK*R+m8{=QERY$jIaXq{f}b@ zOW_@aR3B0X*wQS=R0iG5JkW_>i%nLFyr4RVT!{hehe7GON4v4+#?m9K7@=GdlPWHP zw)Y6t>&qk`Px3i}9Z&7R`>@W2ekJ^|LUR@RgOoHX^Jszc<%it(UNO^-+i+dO)zI?L zNj1?h4euFUYHVz^YONR1fS3!f&PmUSj`PrX7+17cF@o6BejD@}KY!f>EaxCRAYt-U z_1sBW(QZEP2!c8*AL~OX7~Q$3q@aR7QJ7ra?zXRfh?#>7h;cb-T10B&3o{0PnIFOS z-l+<$-^XX#@z{WIU;gYfAL+_V@FZbpBjrq?20YIMy{{Hzo5pfIsPC12^G)K!I*OTrJraxDUq4h^L}=Vq2i0JvFA5OuMGIQ60voNp`%e)>vXf{FIs59mZ6XzC?iwLNtAjolVg~OzLT=_ zVKV8n?*_k%BUkBN8{&n+@F1kCv7a3mj+<|PuBeE&=(q@wrZbC7*$+d8v zw>*NGjpZtpt9>Q?b2s-*(`1Fm%ms$)Ws^|oBIERtvFCjKa(KOen|6Adc0}Fe;&GRJ zM1Sxk?JipWCX^J=nw&pyrb8H|j!uPopl>fz((8t94E&-gii+MJ{6kv0k{>O&s>Sha;JDPISy8BBzota4yYSP`Xa{tCv}a zbAgzys2hdGHQ1#ec3d}1(DrT`FroCB@9G~d=?}z-j?Wd>J+A*=!dut;pXDOsf&BnKLCQh;zk+4D(rccJWsQYf9EA{-N z7sM5aY1%irN;PZshy%n~n8c--BL}q^omQepjWtZDm)jYipA+AGv`+tA&t4}%i%7UD zS=j5wP5z-GgFlAqCqCiH)HPn;<77s^4>tR(2g;eqRySqE%oFz|5Ty|3yObI&*qz?2 zGPo;C`4QdC#``E!n6B#%R9qTW&tM0=MpS{T|6rZ&xe-;$V&Ud_(3}s%fn9W~dr;`M zx+%hFeHtNG5tBiq@4${=4ZIF&5_Lp$9I8uhBiOOk?RhUP(2a$HPDx7xVCuW0J8o7f zqaI;W=hozbcb{qu*KRBjeRa8b)*y8xkDz1EVn2ZC?JAR_0=iw8H{Bx2s)?U zLgM3cF!g@#Hx}fi-Xxv=TNgVJ!reM@UH(VrNs?&Qs!v< zOZ9hCmlSC{O6;{R#42D<^QaH~5Z%`T$`9!4hGq@4QH&_Vlz-tp&4@nKMk*Lys5%B~ zs_Ec!4h?Lsh%${<_2|LFu{ty#)TZ?s-6Ad%myOmR!TNUgn72Cf2y3-Z-AYEeVMq%I zRT&f<@*eEkuCMR<`kN&BPSO%PO+<}k>=tQ+(CkP7r(-&>db#0+lpD~c-dj_aoJ zP!2jh>gRLUu|c&OhCrmyP#sF8kq5~vk^2GY`q(rGdcDTf$!#CxfO^5AULMQ@Nohwx zu#n0GyDVRUj#|<_;FzZUsgC_vk#19%T&z}4I?M+uM2jl(n|2fwh+X6w++X0iS!5Dg zl#C}jqFcnrc?+C%FP9ERKaq8!cvD2Hj(4|3vSIG03yL-yqsed$mU(bjuE^6Vvko%| zy73O`qgi2rmy1Ws)81Pa8V!EDk68>oSn2B-|A7*fv}6tnF>46I;GmW?R=Y9< z#c)_AC|)A;^K^C49KopVk?4ySkq7Ylg^$vjRi7K3eYB{$^4vCiRKwz9^p+@RzVKMU zM2)0NtGT;eU(%8m@SE3Lin&DJ)o@mW=Z9{I-%oc({o_<;s3O8_7p(v zr_P3x!0TM{J0yJ&q+3Y~iLmA<=Ng&n`phy@Jy*c!;K_ zz3|~JF;ULqqZ7>D?@%+qZ_gTu@kSt;{NRrQ%)I38aTmG_{jvp=EY>d^hczF=@?AJr z^jY9GX-5cEBQ*$j)JZr&#s0Swa!$?`P<}&qBEKo*In(4G_^glVyn))HYg6FF?Q_0% z7(Nb;qMYz&;hsq!2C0-kh2i$Va(k$idShUm!bIa&x)VcuWV8+yF|ycek5jM|3!1cO z?jK$L!1L_!V&kPRG({6eYcMXEihKL{ZAFh5ZX-%lgbKfdeGABEirMR3jxxK??e=Jr z*+NSRe`4Z=mvcu_0bE0VeQ3Dwp;E*CIvup7JG*zeIg5Y}$%~}-&o$jkOOlmh(%P_j zw>SzlQ2o8dqz#--&ip4kLW3WR2j`xhA;cNlKxEzg$823{TFJJbvHo`lLP6ymDEH_B z$rb)6^`xajP&#}!QRmXcj{}t<2u08xk7N=-_%-St?Hpu-*9i0q1=}qDgf-`;&Bu{> zvJwV9Qt~LfoLXUwkStw(Z-3ptMKYD1KDkW{{CdR%iwKe|(DXjWAXsbxo^?nT$~?gz zm}byn22VL->%F#la(nizKfB_YWLP`I{@t|W3OIxxvRE)m>b1*Ev+M7(=Oo9y;05YC zgYw(-bzNiMmh=$^4x$qW4A&FrLhc4GhZ9~aHq$$lYoEr3OvV zJa<`ccAUQ><$D-jc#l!YInR`8OzkU@*TLF5o@SrE&9L7$5p8ysA=DiqJ>F5PNU7wH z9OrbpV(};9bh1h;3#MDO)=v6cyYbBW-v0EoEAYsix9r$4 zi|`$Blkpn9N@|O-rLYxlUNMnRVF8vl zOw{1V&+M}g@HUxezGk?d2tf%GZ`sr2$<0P!iDU>|maY>FzMgh>=Unpe*P3mW{5`Dv zcbM*6KN6=xeFJr_k%hzm+Q4agL&#>S2T8|heMoYz(KRUQ>i6x)ZX7&V(Xhe{?)DZu zYaB|ELw`1aYQEeQ-Htq=L}s(3CD|$?B^&REtmiV$2NZ};lynNcg_r|H#`_=&BnX?L z7S><>qh@Bz(IAIy`bGsX@bW~UwgS|rY4GQk-iq+QzLGSc%>)td*y`9I*qdw>J;a3P zJo!4B`@XEf$4=O`WVvd;3;O+IRMZ|@iEf#;q^0A)JQHt^(?|Bqw%=zZKKg5*P$eTg-v>?``gH4z+w@*X!05AQ=8ka~_tos8}&9gDQcQqXwzC03L zT@kNTm)3?QNs}HZukhry!y`1`$qw$7?ir&!An8NjYOs(Qi+*wNaMZ4ldsn(uG0|h9u1L(Q{aumb6orqK?o5 zikSZVcW!EZfBz{k&erdF3d&1LvCtc>j5>?jkj;!xDisdm03&>k_;7Z6b~^XvKDtz!;} z8gmA|QQ9N#)Ij;C1R_u#SGQA~PJ}}hum+J&7bx|^{IHY9POF^Q%m}*fZ~i2C?3=@z z{Eaz*6YzcZ#R$?T3LYwsCOa_Ke2@s|OuwtzC}jUjN>tP$J%sZ7dT>EdEcB8ixsRFD zD`_&vi@#Kp*i!38H#eaRFPsR+LP1%bj0eI5-u5%gpvOCWBp0?jl?B~|?IUY2rr)+T zf#u{!;kp}3F@mT&%!%^&n!~C^AUO81LFheD4>#@?x;Di#WPaCF^scRK;rwg+E8o}a zLjd8CJTnUqQZlVuZ$0Hl>9KUj$yJG{xL}JLQN{iw@&m)4;&yT2jVmL;C9ikebALCD z{gpIm)gBE+-R~}#^6>Du$E{P2Uk||MyKNMeF)nwDYx1;m`D>IW`|5 zg*E`c!+Y$CRAK3fCf0tw;P>i!F#GmA)Dfs3c|T>9xtOj#i0bDLzJBp;&|DHX6liWZ zENo3<{dP$qC4#DcBJ{w^+r$@X%bQt%b4eX@fBdeyKY$eK!h93hs@E(O9q|DubEpK> zfAZQ5tS_OXzU7J9S5NsB`N@w#3pB@T1f912n_vcC?_3utD_OxqnU1pxgNUl zh(tB4xe;Vo(#;aHSM02?)BXkb`MqalmWd+Q*J6WW_a68p;`aKWsOZv#Ws(k?PfAei z%IPPEnS)TW!6p7Qp6j?5>iIpONw18KWHx0;bS~_yOqc(3Iqo8w_=ecoWC@ezbTS7@ zy)1!R(lIGsT!WksCv&?^Y*5Z@i*f+&eK8q}oLy0DIbU)`jJ-^pWZJc$fZ_HXi$+fD z^u2V=Zvw#9!#sS2U-a3<2Qr)LX&=6@P=?2B&Kz0bn%WC2{VEZ*gAac#pY=oy9`(!YmZKupO^%OgF;mvB^I!mxA@iQJ$crCN6;&>j?U$D z_(b@SdSL?acuBmGt^bv}PQzHF(?JpW1+HXfi5d3I%y2F%0G+cKy|L>nJAj^au{<@3 zcj&!(5wjnW=lWV^kl_GOecY%1~+`A zu*e^%2fQ=QQ(`GsF!#vd+EaF-*p5Z}`J^N7i8>v(zq3+s^Bhx82H1S0=66UNJ<%aP zB@IKBa8XfX1#Vo%u6IAVlCMh4==}M|%XogTD-?s-{q@$ch)D@kjFs4@L-gfg{%)6} zyS{o4^H`*8N^Xo(CK$bM`3SDB_kDvB^iJ)e=pbghu#yiiWhnU3Y$m37;5$%YxO_rA@`A%#;57MSG-5NeRhHl-3#_AO+-D#|{V%KE`X zqe3MV>giJv+1Ij8DMa=Zvi#1|?~mW#zyG{E&%MvN=bU@*xo6IOzqt;!fVsXIP}q%g zC*eJ-YJvcDCD$6vwOr35aX7vvEY6Zdi{e?;t}G6bTRC6X;uXOpvIphV0IKG>9Sfqony2=CdV)N=Q2V&bfV-9W$r4(2vBfzk zf0{rxL1Vrt*rAKQRd}NK;pi7W@#qp#)qj~9%jsVh8lSQ6(^-n$eBuqA1OdQJzkuV_ zdTa4yoT^%{N*~H6@@oebhbvfW+}NQ1m_~Kotu!Y8b`kVXJ$oDbC(KiASdGGO4EWyk zb~UDeRCAWJA@{R}&|5)65)=;UcEH8vBF=E2sqo}GK!&nL9_zv){X4-ys7kuJ^ zt=COM4A}X|eCmsCWyTkUPYSTthyk9euQJAjbCEPJz_5Js2(XGf!b_ot@p=vk^Y{Vi zft9u*@BWWYukO9;eDZGr?0ni1&$pQAb7e_BE_zqaj3Y(Yc;i`UyaUmFM+ni6tFkCC z334uDR}gKa`C2WaYp#^)vvaAWeb=bs{t!jKJ<>1731o%!ql#O!DE`iS+HJq&7vzj1 z5nWTI3?Bl(Va=f$AUnUpyflj6{!EbBNvdW$xO3HFK7((8W9|JdH--z~$S>X=dlN12 z@vW{|#Mk{`^ZEhkAj{`qc<1B0sPcOo0}=mgVW?#PTwPNkeC&KqPp{`olzHnlDjf`b z5WOZ(f~oHK2LK|PGHVk)7gYBPi0R9?yyMM#VcQF zvV2GLWg$}b!w5ow2t)eiXTldV=;QX;CT#X7 zQRkpY!#Ee_iY?98I+`m)ZgR)k*aZGh=oj;6u)G+Czl)MTi0GPe`LjvqPZ6ZsX-T+t zTt7e@Lv|vEJC|?cn!wmfvY9A{M3yl5dV*olY*{)dwBWc#dtge+1 z=^<^`wz#MHXfrLDM2aK2DSUsSNd3w@&;Ib)7($qogt7}d9~;- zX}k&*_1`C}2qm-*JS?t)p3VI%K$(Wi3N8mB^+?SK#NiusoKidQ)1g zA`>v+Zkellwujd3^~Vov+r5;U2>~}1RvdP_1~~erCqZH7n{OwNa{A2Y$y~1!6|sp2 zq+v@NuTE(h@-?SsyU2Wt`1!CVOI|r~>=Cx(=!ZAs1yX({|C0{G)oVU3=dSaD3d+7D zf3QAj5_|7(c&EQoB`~_gTgJAswzEYaiN74tWN9O(D=3m;pite#Lm8^R?DL)7DO>TA zITAb7-K7zM{g@^o1qRKw)`*X^h=a7V>8^RRrN%}%->6+=F4~W=7!?@bEp4Ra+G=*F zL|MAFN{{)gAG0|2RaVY9DbgWq zFO2)0Gh!(CW#4?N5^UmaSwDi9Eb}~y?~Ix59E%e30$%ZFFJjp7akv?+f%y2a@a+1> z{)*BsCke9Zw;vk%?XJuZyqV#tX7@NQ%G3if`yO4>8rCiHh7ao!|6E}L5OL_OjPrbzLed7X zj`C*sxWX(w9Hjj?UTrJ~xsI?HZ#0A^svUX4jrDD+9aQ?;)ZZ6=@W)f?L}r>|GN8tyug={QI+na&1|W!I9xiO zo3^aDBLc?2z9OIu0=71BbDsF@}J%q^$(e3C~_Pz)(@u+XF#=5V{1zVx}03}ZO) zKu*|0=I1AV^w``la6L8^ZFRPJO-N%m!k}NwQVTzYVQ7nLZ4< ztT|gCZ74M~plhb&zrWH@`Xz+o`!gO&g%^A5tE65|j92>@c$h`r%h`nMw$H(}`_HBm{3{P)rS@wmyX9B zr<#3zGpgV&qQhBhG&4@lhNyUc=wma8`&0nt?)(xTLmZ?CBLV|&uBpzaikQ7rYtBWvc@eMYXJg-61Q~trVSQ_=bYEVu7#SqZfZg$ z{NGZ9%E@skvkr~FcFXzEMK%Wc_JnZ{(TBVk|L2kA!54SrJK{NlNwT6oL&Rr4s&sDd z^N)Jpd56dTUVPbybah{`kbkUQj4KmrAJ(1gk?S(d5A=Q%{Nr2h^4ezf*zb!Iy04x> zYNF#uuKr|XbjLev9FX5yFSa~9u+GKE1vFPa@3{2n=qVDgBb#>!RTVa#L+KL}b?6g) zF$vTLsFRs1m*2TPVgVd-BlgQzs53DH^q&XrjXYWMMwy0Z(?@S>{P-xyZU z6#e6LA=k0xexOu=pIpR3Rbyr?ziC1}U@Lr`F_Gz|`2g?>&Bo`9nB4RLPw1n3!janG zwYTLLUwPWk_GqIk9$&r544|m4%uZpmO=&+Ge&XtARFkew^itUBJ zTp-_t-wd#COa?~0CPRL3MefnCoUXq}Ex-(cw;TRaA+OpcOth_1MUy;f{@)4CDo63r`aaiX>etha*6r9hwTBZU zUe@+~u7>PkegEU^0oITbf@zKZLFpQo3ZZ=WypeyiN>z^@t-@ZL`MJ4xerb4mbV+X7W+rwLD&CW zK>C&nNU-4SVx%@te4K^neT8BEBQlvopi&jbGgT;b$q!2-hCWf8SwIdS9WQFPG=YVuVq!dD{a_PEY&%$AGC| zZi)wG4LP(@pk0M+%;Vud#B{5eJnq@M@uw-m5BCH`MKC+oYT8^4nJ4It8$6`JgsH?F^T+d_O^u9?i0Eo z)%Y$7J2_lk=WHrC-7br^z1_B61we|OuaXjyIrN+KEon-G;aQb=S?bH92l_V(`o8?9 z#x~JaJwL)d{1lmqr7xX2d#6{`+l`1kpF|jQmP1|t%=tmL zKrbUFoJCk>q1p{^w>XW}6T9ym{Q1zxdGql^=hObq$+{4e*B_8&B(Uo9yB;u0x=BR} zf7BL z;A|LE<+Naj62evGd^(GLv<_)X_%oTARk&m}xJ?L*b8}~NbDatd3=fP^+0gsFEmeBR zQ*01pS&O7ik3 z()`)o6b3VhzuAE)owDi7mJtmhU*GB0wI=Wsjb!C1D-bn5f>Kf)zXCy1@ z_G*5C-W#pjaTH0RC39Rmm*;-BTt44C0cu)J+ zRZ7sU^~QO~Z3$U^yEi2QxY>oE>Uk`?S?T?XR@M;z+AiICGcz%3h8~L#x84qyqOHdg z3yw^u-pCU{i9`~I^1)@Sm9YhK znNUvpGlWlzzUZ(lhS(s-(Fe=adE8YyrHa2Aaq@N(T@`0)F{56qPuq3iZaT~XCu|Yg zzjNCI+yG7phF_D9v&9`)ICYXl+hx)ghq0%w{QWPcS~iot*yLwBEPjICZk(%}l~H~- z*N2Y4kC4w5!9etU2M7y9R~u3%tF3-%L6xjGh<&vN zSC`rX)efihV5k?=U%}E0aK^XT{)=Qv3>wh`i#*_t*KYL+j0D>ewtnvNF3NqxP4dU6 zJILKoG%=5B0;Xt6Jf`br?PGSNH5^V zl2VB)&$4TOP?5_fHe;^dgaYGi)?6#4NfcCEtteX)Ys6!5+qWX_mX7v$5N`7wC8iOyyW>wb}`&?zEPf zE)^P`j}h+3%t2I;me=ZHKK$654(Bc6E(C!Y*Uj1&Cw@X0hZp;IlDhAgg|jmKAloy- z6bpXytx8oB_R9-CFg4eYwb^@FYu#I1(mI&w(2iCG^TuSq;)=w7{71(uPbJgAHa@xY zV*nz(@*tZZ-=5TdbW+c#u58{I;y!{bXv+`IFyYF4^1)(VTs|B!ymEYCPH&2doEBh-8-cTc4~AbzuJB zSh4pfHmM0Q5S$VR+xB|mJ=1}_08DzUlaFt6FVm0G#uWQrgPf)ZoDPaV0~Td-9NJ!= z597^=Xo_SWf;KhiqLO9&XYI>Hgo<#zR^NfRQ0&NeB?(1g;4((Xm)DU#q`ynC+xR?Y zjQxOI<{J6|!QKr?iWKt%rH(1)Wt7r=$?7>24mW@gt4(>krB%D0+lFd9!F=HUi$1Jm zXAU3Pb#$3M=E%^krMTDX-T$4()TOmN?UJ5G{kZ&o_@M>D#tWy*5rkDurUWyB94=dH zL3{B42&Ic-OsI~r7wKltd9*${{(I`qt0_xY^0GnIgR@Rhd}_WR;wSHCz|a!{hsZm& zj70>IU$5o5Enzx*o#I%L2e%yP-8c>PH74-yFSQa{2@aRQ)#y}*<%p`BqkQ;&sQw&n zBa)vJb&MNkw*TL;FAyLzJnJrunEZE!e3rZLA618S_>tf2`9~2Go z94fef--VW){lv?_SJpM;V(ER6I_D2oiV4A=J|=Cg?&}{MY5%mWJGuF1E4rhp^>X{k ztNNjhTECtZ%umiB(!9mln)sLOXu(fXr{16|!F|~wj<|BTO_^5=Zk({}W+@A|XYQ}+5&Oe3V0m%IoO$>!>&M7ABK`fCx?Q2` z*D%wdTfM+@?UoR!!f3sfoJRMzUw&OxFcN28YU%OQmH7|(xq^=A$X95w=)JXP_O6B3 z(bnQ50<2=Z;E`(Q)#te-%yl0OcT=@?0lpL{aa1n>rfXar?eF&5yoUP zUs*I+cE|VXafS&lunnFowD*Xm=bvP$zu{1RlkeDX(AlYm>>B!}(ZV@!e628!+U#HR zt8y=+ZOLN{a8>f){Y9O69+y~8KHuS5IUUp3*Hn}4>}LZX@;Gw&j=&kY@xvkJWR<=3 z?erGk$J(?T#~}i(_%}(69iP+0MjD^^0CDIOsI`oJ0}bEzZs3?_^maV>ykI@}(O;3V zFEKeyjp;qgp@j+K_2Z~K4QJKzU4Ws{0(X?e^!Oxb=H(M@Ub1m14=>zl#T_V3A}qs} z$s3bBo-G|8NAG&DW{NnMt0$Hh=l!_z`P>DX`J;)b znAn&BTn9)jKM1r_k#K7#2i|?wGx{Wev(h){mhe|wrYDk#q(o(yb&vPf8!QA27AvrS z&S_T5ox=e`22}5MF$0;YRlmGPakT81E!V0jV;GL-ipFiaJqI}TF=4@e zNy4+5$Y}#w(akT2wAT-BVEu>$igVBNI74i?Oy(!BWFx6fynC)BLUGoyd>~j6Wu3z>lGHP200ryV}ikyF9X=8F{?vlxNf(U>hYVB@~+wVP2{U zdPfr$Y$(Mr%?=OqZzOAk=|#4@NwB_`*|)($aT@%0alzAx5^y$44Ff)t-tr9+AGbh% zHi_Ax@Eh45aaB@7sF3f%s@!ZwTt!LW0wbJzu;Xq6HOectk*(0CoB3QUY%gONE@3}v z1v%fAt+6lz;YN$V30!MQ-oh&0Z$HCjcMfsoXxk9BgDnJ~Zzrx=&F@QN8&jkQoC>;D zr2{T1@7AV^9mD>DHTp&?9&x{L`;Ob6_X7@FlBeVzk*=_=&FSf!*V_nw`hK6+`&M>7 zxlC;kG+WEh@|OK_5Hr0?ly<^YTyFxSiGiF(YRjWSN3jF5OC2LIp4d9fmCnXQU)iTb ze@Q&1&X|Fo= zM%rXrxduPKCvz`k4blR@OQgKFI8ahLx?ql-?(E{gw}Q0~uiE7@>tx^bvU`t8gz5O! z-(k}e`Y0%6lkozsXz0_IDy9Uy)KJe%$^MNwP}K0B9XWJAZUTFCyz8@vL8ZV6rphGt z*m5$`6O#w#?4c4al&Wgn;)+#?NjdMT9(%m&*q^c% ztd#I&$+T?<=Unmg6Sav?l*aafwW>}7!u_=VEVPlsaz1iBEF1O%O?=IxiNL+HYYRh` z=T4tE?bN#=Jjl3Z6Qh82?lMpC&AK|`eSN-$rz}I*LwX?uJ*CB~9S4Q2=agy_lUf@P z0+RQ^F!bT)nKOfX1Enxo<)3G|Iiqq*8qCh(DVyI4GGC2R?p=L}J;?eo*P!j!Pd8d^b zMWMBvR3cNJ$w6K-gL{0|AjOM9I2ERQm1_6%mcto%x`>^sjFXbdl%TNiP5{qzk{Z za3Jz#Ih12V*pu;du%r@6*&MrZy1JJxd<_4eNjI}B{36iMw*754PQlLZ=n{jwtaw;G zVk;(gW8GOx%u0c0W(A2wZpWmPfaNE#R~g)kHznJS#enM}1PCNrRaP{2eS$+Yn;UFY31+VV)pAH_$Pi#z7JHOB0i3&|8OFT zx*X#QH+M+gN7wlmPBQK3O9habx(IT5nW@`B$db#Yr&w!F1F`qUj9Z>F0wX(uk*k!{ zFO$Tzli}(M9HxB_%s#7dg3#7=5|JidB*P>rrJA6F=sx_Mu&U*!!Pu9OoW=&y5|d2Z zuHfA5qA3K@=S0SKTgk{%6e}!8W zUKmCn^{NJ5*d6Z%qVRUb?`lK9f9yg^jq!U+Xj6Gj0$1d$hw-dD%1g$EA3EC~BgHT++S(Ag2+ zhy5rzoaUa_M$Oa~*HPio#7bS6IJM#}m7cyLT7DJT`_;4F?2FH*jZz{z`v+eHK6YH! zwBCPF=wzA!$J#TAGpvI$MRz%k=!J5OPEM`v*sM28_Dsa9>g}xoZSkSW zuFr#4w#7k1W=e(Qr>v-SoBo~_`an(a@E6GM~-l*<_~U3@KB8TT|6B<`Jw#zfvM z8x_(w?Os&%vm`4h7LMyMxkYp!LtuwTTr(zpe#A@4et#>f1@IV|o&NfAL9JYFvf%XM zm^qe9hkPvC&V%A@vwGC5rdIWC@PUqo_!n>SGA@ch6V8B$oqoK2vXy(nVDYdC5vh&c zp%G%1lBO1c3PaSk!HilwFY-9K0E*9kTPYo^4f3ly+9v!{aod&i$lp(u>Ku8#^PBve z8@(fFqae8L9Jy;%xN|oQk8az=A1SqZ>$J%0edY5*dZTOz=yhtt+T+-|lK2m)3 zfa9jmPZ8316U>E?e@g9+=JzbFPr>?zal6`X#dNztS|8oOUizmr=;}~K;v|w^qzp0r#i>Y?b3+q?dD94vi0gI zY1)evstN69%U8A7fSDF0ia*+Ta*%ZxJ8bN@y&tqPcMhb>tIs`9R3G)N7-p9`jUKkm z+K;Kh^B?WAI5TNE%?f(yo@(}z@nii~mduZ__h1KvjZfxQy|p=I(b8cFbV(2BBq=Gk z1h7&J`1U2i*-Ff$PO;|SVJpXZ@J&W2_$J9ixs+ScDcFTKR~)<|Xlb2VfFCO^8?Z=8 zBOdgFBfh<@Pz<25V5Hben)tvvF>erMYF*4Thm|d+J}pQX)C`y7qY%f8y}DU=Aykay z(?W9I8`sLBSZ;LoP;}+Ihrd5?l~cbC&w5V=M2M%xWFjX) z%Z%GUuybtDrjCX?^v!9u8R+xp0jJEvk2l4|pi_^m8L{hx$xShFaE|U~RKjfOQaQA5 z+%oz&eHPb``;Fr#_u*e(ft(s@$F}qynZd^%sFA$?=bQ2GYq22pvyB^^r zNU`i?@4Mu|8$C`80Ym%mU&{er`Ly99PaViY`Cq-TXe7Irh1GWxrX_H_oDOROgo9=5gi! zvha_!$S{O{6b*+w;X7gHxUCG*1nYq`B$IDJ8s9XsJ^=CG&yPm52vb8KTjd|NaNL%L zchr+X7`jKZ*N%mdmP~>Z+0A5TJCA1mMw+Ra;cTaIH2E;sy)v}*aAdy2=K>0tdzV)*2yGPK=!u1$ylBR_%VhnM2cc?|1 zIc_UJN5Pt?P{oR+_mFcY$#-GwNyTxen%*E4jOo(Y-f8n5GgEox+yB6vX(`vAMwwGQ@E^HD_ zdrCj>Q)WS=@_DciR&sGxp4NK=rWP$*rZh0z>*pQu7bNdwSvV)eB?gpFZx%j)(KLcM zAY;XwrxV~@Y2_7k+`eryi+v59p#L-ad1#Ge<;6B9QoLqVAZt|HX<{}%# z;Gl}%UA4hqJ$#}=F^13Dbs5vLX>W+6@eZ`hiua6^kRK;7E{DVWn<69cGjxU)_8zLn z58aLkzBqBd%vgP1Ync{NMEM`QsIr@Cv6(ZBndB_u(IwBTer>hZ+F;twQ>zZp;lE>YlMr|#D&OWg>qKs4lvr~h(t{XM zOSN>)@H8e|ecP5!WT4ZM-k#cX#OmjrKAm>Vd7S4n1Aa-*>h{tm9^d)~L^b1rO4tw9 ztrc3w4Q>)S7PSgF@S9pr2!}TBoqnD58+q_W$+AHv8?*rfyuEd8v?O!;s8u KP0Nib|NB2@K!5T8 literal 0 HcmV?d00001 From 60960cb3e021780e68a47466a89e8e9ed25a80ce Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 15:51:54 +0700 Subject: [PATCH 21/36] Refactor: Update server progress bar style - Change the server progress bar style in the Instance Monitoring and server table. --- .../src/components/servers/ServerTable.tsx | 126 ++++++++++++------ 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index 02ceb43..57dc751 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -5,7 +5,6 @@ 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"; @@ -150,6 +149,70 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) } }; + 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 ( @@ -242,53 +305,28 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) -
-
- {cpuUsage.toFixed(1)}% - {server.cpu_cores} cores -
- 90 ? "bg-red-500" : - cpuUsage > 75 ? "bg-orange-500" : - cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" - } - /> -
+
-
-
- {memoryUsage.toFixed(1)}% - {serverService.formatBytes(server.ram_total)} -
- 90 ? "bg-red-500" : - memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" - } - /> -
+
-
-
- {diskUsage.toFixed(1)}% - {serverService.formatBytes(server.disk_total)} -
- 95 ? "bg-red-500" : - diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" - } - /> -
+
From 3f3ce9122db7ca1f08cc4969b81d260411844ad7 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 16:35:28 +0700 Subject: [PATCH 22/36] Adjust the size of the server title header. --- application/src/pages/InstanceMonitoring.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index c8dc476..8ab5641 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -103,10 +103,10 @@ const InstanceMonitoring = () => {
-

+

Instance Monitoring

-

+

Monitor and manage your server instances in real-time

From d17a70dfa8f374732c28fd1198e0be4886ecc4c4 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 16:38:57 +0700 Subject: [PATCH 23/36] Updated server schema for migration --- .../1752916282_updated_webhook_configs.js | 125 ++++++++++++++++++ ...1752921327_updated_alert_configurations.js | 42 ++++++ ...1752921397_updated_alert_configurations.js | 29 ++++ .../1753085447_updated_server_metrics.js | 29 ++++ 4 files changed, 225 insertions(+) create mode 100644 server/pb_migrations/1752916282_updated_webhook_configs.js create mode 100644 server/pb_migrations/1752921327_updated_alert_configurations.js create mode 100644 server/pb_migrations/1752921397_updated_alert_configurations.js create mode 100644 server/pb_migrations/1753085447_updated_server_metrics.js 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) +}) From 2d2122d2e5842c36c0598003b1c65317a33c3540 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Mon, 21 Jul 2025 22:18:27 +0700 Subject: [PATCH 24/36] Improved the Server feature History Data --- application/src/api/realtime/index.ts | 2 +- .../components/servers/EditServerDialog.tsx | 16 ++-- .../servers/ServerHistoryCharts.tsx | 6 +- .../src/components/servers/ServerTable.tsx | 45 ++++------- .../components/servers/charts/dataUtils.ts | 58 +++++++------- .../charts/hooks/useServerHistoryData.ts | 2 +- application/src/pages/ContainerMonitoring.tsx | 14 ++-- application/src/pages/ServerDetail.tsx | 6 +- application/src/services/dockerService.ts | 38 +++++----- application/src/services/serverService.ts | 76 +++++++++---------- .../src/services/serverThresholdService.ts | 30 ++++---- application/src/services/templateService.ts | 30 ++++---- 12 files changed, 155 insertions(+), 168 deletions(-) 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/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx index 9c6b618..cce5755 100644 --- a/application/src/components/servers/EditServerDialog.tsx +++ b/application/src/components/servers/EditServerDialog.tsx @@ -78,7 +78,7 @@ export const EditServerDialog: React.FC = ({ // Initialize form data when server changes useEffect(() => { if (server) { - console.log("Setting form data for server:", 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) @@ -109,10 +109,10 @@ export const EditServerDialog: React.FC = ({ // 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); + // 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); + // console.log("Found existing threshold:", existingThreshold); setSelectedThreshold(existingThreshold); // Handle the API response format with proper field names and type conversion setThresholdFormData({ @@ -166,7 +166,7 @@ export const EditServerDialog: React.FC = ({ const configs = await alertConfigService.getAlertConfigurations(); setAlertConfigs(configs); } catch (error) { - console.error('Error loading alert configurations:', error); + // console.error('Error loading alert configurations:', error); toast({ variant: "destructive", title: "Error", @@ -183,7 +183,7 @@ export const EditServerDialog: React.FC = ({ const templateList = await templateService.getTemplates(); setTemplates(templateList); } catch (error) { - console.error('Error loading templates:', error); + // console.error('Error loading templates:', error); toast({ variant: "destructive", title: "Error", @@ -200,7 +200,7 @@ export const EditServerDialog: React.FC = ({ const thresholdList = await serverThresholdService.getServerThresholds(); setThresholds(thresholdList); } catch (error) { - console.error('Error loading server thresholds:', error); + // console.error('Error loading server thresholds:', error); toast({ variant: "destructive", title: "Error", @@ -243,7 +243,7 @@ export const EditServerDialog: React.FC = ({ description: "Server threshold values have been updated successfully.", }); } catch (error) { - console.error('Error updating threshold:', error); + // console.error('Error updating threshold:', error); toast({ variant: "destructive", title: "Error", @@ -301,7 +301,7 @@ export const EditServerDialog: React.FC = ({ onOpenChange(false); } catch (error) { - console.error('Error updating server:', error); + // console.error('Error updating server:', error); toast({ variant: "destructive", title: "Error", diff --git a/application/src/components/servers/ServerHistoryCharts.tsx b/application/src/components/servers/ServerHistoryCharts.tsx index 688c69f..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(() => { @@ -71,7 +71,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => { } if (error) { - console.error('ServerHistoryCharts: Error loading data:', error); + // console.error('ServerHistoryCharts: Error loading data:', error); return (
@@ -122,7 +122,7 @@ 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 (
diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index 57dc751..89c948f 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -59,39 +59,26 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) try { setPausingServers(prev => new Set(prev).add(serverId)); - if (isPaused) { - // Resume monitoring - await pb.collection('servers').update(serverId, { - status: "up", - last_checked: new Date().toISOString() - }); - - toast({ - title: "Server resumed", - description: `Monitoring resumed for ${server.name}`, - }); - - console.log(`Resume server monitoring: ${serverId}`); - } else { - // Pause monitoring - await pb.collection('servers').update(serverId, { - status: "paused", - last_checked: new Date().toISOString() - }); - - toast({ - title: "Server paused", - description: `Monitoring paused for ${server.name}`, - }); - - console.log(`Pause server monitoring: ${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); + // console.error('Error updating server status:', error); toast({ variant: "destructive", title: "Error", @@ -138,7 +125,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) setSelectedServer(null); } catch (error) { - console.error('Error deleting server:', error); + // console.error('Error deleting server:', error); toast({ variant: "destructive", title: "Error", diff --git a/application/src/components/servers/charts/dataUtils.ts b/application/src/components/servers/charts/dataUtils.ts index 20976fe..4f3355e 100644 --- a/application/src/components/servers/charts/dataUtils.ts +++ b/application/src/components/servers/charts/dataUtils.ts @@ -50,21 +50,21 @@ export const timeRangeOptions = [ export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): any[] => { if (!metrics?.length) { - console.log('πŸ” filterMetricsByTimeRange: No metrics provided'); + // console.log('πŸ” filterMetricsByTimeRange: No metrics provided'); return []; } - console.log('πŸ” filterMetricsByTimeRange: Starting with', metrics.length, 'metrics for', timeRange); + //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()); + // 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()); + // console.log('⏰ 60m filter: Cutoff time:', cutoffTime60m.toISOString()); const filtered60m = metrics.filter(metric => { const metricTime = new Date(metric.created || metric.timestamp); @@ -72,17 +72,17 @@ export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): 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' - }); + // 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'); + // console.log('βœ… 60m strict filter result:', filtered60m.length, 'records'); if (filtered60m.length > 0) { return filtered60m.sort((a, b) => @@ -91,18 +91,18 @@ export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): } // 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:'); + // 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)`); + // 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'); + // console.log('πŸ”„ Returning most recent available data for 60m view'); return sorted.slice(0, 20); // Show last 20 records } @@ -117,7 +117,7 @@ export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): return metricTime >= cutoffTime && metricTime <= now; }); - console.log('βœ… Filtered', metrics.length, 'to', filtered.length, 'metrics for', timeRange); + // console.log('βœ… Filtered', metrics.length, 'to', filtered.length, 'metrics for', timeRange); return filtered.sort((a, b) => new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime() @@ -174,27 +174,27 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => { }; export const formatChartData = (metrics: any[], 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 - }); + // 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) { - console.log('❌ formatChartData: No metrics provided'); + // 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) { - console.log('❌ formatChartData: No metrics after time filtering'); + // console.log('❌ formatChartData: No metrics after time filtering'); return []; } @@ -213,7 +213,7 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => { ? 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'); const formattedData = sampledMetrics.map((metric) => { const cpuUsage = typeof metric.cpu_usage === 'string' ? @@ -269,6 +269,6 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => { }; }); - console.log('βœ… formatChartData: Final formatted data count:', formattedData.length); + // 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 954f1b2..04e0365 100644 --- a/application/src/components/servers/charts/hooks/useServerHistoryData.ts +++ b/application/src/components/servers/charts/hooks/useServerHistoryData.ts @@ -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 = [], 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/ServerDetail.tsx b/application/src/pages/ServerDetail.tsx index 1a432b2..9b714e7 100644 --- a/application/src/pages/ServerDetail.tsx +++ b/application/src/pages/ServerDetail.tsx @@ -22,7 +22,7 @@ const ServerDetail = () => { const { sidebarCollapsed, toggleSidebar } = useSidebar(); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); - console.log('ServerDetail component loaded with serverId:', serverId); + //console.log('ServerDetail component loaded with serverId:', serverId); const { data: server, @@ -48,7 +48,7 @@ const ServerDetail = () => { 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); + // console.log('system_info is plain text:', server.system_info); systemInfoText = server.system_info.toLowerCase(); } } else { @@ -64,7 +64,7 @@ const ServerDetail = () => { // Combine all OS information for detection const combinedOSInfo = `${osFromJson} ${osFromText} ${osFromType}`.toLowerCase(); - console.log('OS detection info:', { osFromJson, osFromText, osFromType, combinedOSInfo }); + // console.log('OS detection info:', { osFromJson, osFromText, osFromType, combinedOSInfo }); // Check for specific OS distributions first (most specific to least specific) if (combinedOSInfo.includes('ubuntu')) { 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/serverService.ts b/application/src/services/serverService.ts index 0f25142..1bdda23 100644 --- a/application/src/services/serverService.ts +++ b/application/src/services/serverService.ts @@ -7,7 +7,7 @@ export const serverService = { const records = await pb.collection('servers').getFullList(); return records; } catch (error) { - console.error('Error fetching servers:', error); + // console.error('Error fetching servers:', error); throw error; } }, @@ -17,48 +17,48 @@ export const serverService = { const record = await pb.collection('servers').getOne(serverId); return record; } catch (error) { - console.error('Error fetching server:', error); + // console.error('Error fetching server:', error); throw error; } }, async getServerMetrics(serverId: string, timeRange?: string): Promise { try { - console.log('πŸ” serverService.getServerMetrics: Starting with 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:', { - id: server.id, - server_id: server.server_id, - name: server.name - }); + // 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); } // Let's first check what data exists in the database for this server - console.log('πŸ” Checking all records 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('πŸ“Š Found total records for server:', allServerRecords.length); + // 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 - }); + // console.log('πŸ“… Date range of all records:', { + // newest: allServerRecords[0]?.created, + // oldest: allServerRecords[allServerRecords.length - 1]?.created + // }); // 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)) - }))); + // 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 @@ -67,9 +67,9 @@ export const serverService = { 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))); + // 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') { @@ -96,7 +96,7 @@ export const serverService = { const cutoffISO = cutoffTime.toISOString(); const filter = `server_id = "${strategy}" && created >= "${cutoffISO}"`; - console.log(`πŸ” Trying filter: ${filter}`); + // console.log(`πŸ” Trying filter: ${filter}`); const records = await pb.collection('server_metrics').getFullList({ filter: filter, @@ -104,22 +104,22 @@ export const serverService = { requestKey: null }); - console.log(`πŸ“Š Strategy "${strategy}" found ${records.length} records within time range`); + // console.log(`πŸ“Š Strategy "${strategy}" found ${records.length} records within time range`); if (records.length > 0) { filteredRecords = records; - console.log('βœ… Using records from strategy:', strategy); + // console.log('βœ… Using records from strategy:', strategy); break; } } catch (error) { - console.error(`❌ Error with strategy ${strategy}:`, 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...'); + // console.log('⚠️ No records found in 60m window, checking last 24 hours...'); const last24h = new Date(now.getTime() - (24 * 60 * 60 * 1000)); @@ -133,35 +133,35 @@ export const serverService = { requestKey: null }); - console.log(`πŸ“Š Last 24h check for "${strategy}": ${records.length} records`); + // 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('πŸ“… Sample record ages (minutes ago):', records.slice(0, 3).map(r => + // Math.round((now.getTime() - new Date(r.created).getTime()) / (1000 * 60)) + // )); // Return all recent records for 60m if we have any filteredRecords = records; break; } } catch (error) { - console.error(`❌ Error with 24h fallback for ${strategy}:`, error); + // console.error(`❌ Error with 24h fallback for ${strategy}:`, error); continue; } } } - console.log('🎯 Final result:', filteredRecords.length, 'records found for', timeRange); + // 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)) - }); + // 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 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 index 18a8162..4a2804c 100644 --- a/application/src/services/serverThresholdService.ts +++ b/application/src/services/serverThresholdService.ts @@ -23,62 +23,62 @@ export interface CreateUpdateServerThresholdData { export const serverThresholdService = { async getServerThresholds(): Promise { try { - console.log("Fetching server threshold templates"); + // 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); + // console.log("Server threshold templates response:", response); return response.items as unknown as ServerThreshold[]; } catch (error) { - console.error("Error fetching server threshold templates:", 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}`); + // 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); + // 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); + // 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); + // 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); + // console.log("Create server threshold template response:", response); return response as unknown as ServerThreshold; } catch (error) { - console.error("Error creating server threshold template:", 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); + // 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); + // 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); + // 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}`); + // console.log(`Deleting server threshold template with id: ${id}`); await pb.collection('server_threshold_templates').delete(id); - console.log("Server threshold template deleted successfully"); + // console.log("Server threshold template deleted successfully"); return true; } catch (error) { - console.error(`Error deleting server threshold template with id ${id}:`, error); + // console.error(`Error deleting server threshold template with id ${id}:`, error); throw error; } } diff --git a/application/src/services/templateService.ts b/application/src/services/templateService.ts index fc68e6c..f1c37fe 100644 --- a/application/src/services/templateService.ts +++ b/application/src/services/templateService.ts @@ -38,62 +38,62 @@ export interface CreateUpdateTemplateData { export const templateService = { async getTemplates(): Promise { try { - console.log("Fetching server notification templates"); + // console.log("Fetching server notification templates"); const response = await pb.collection('server_notification_templates').getList(1, 50, { sort: '-created', }); - console.log("Server templates response:", response); + // console.log("Server templates response:", response); return response.items as unknown as NotificationTemplate[]; } catch (error) { - console.error("Error fetching server templates:", error); + // console.error("Error fetching server templates:", error); throw error; } }, async getTemplate(id: string): Promise { try { - console.log(`Fetching server template with id: ${id}`); + // console.log(`Fetching server template with id: ${id}`); const response = await pb.collection('server_notification_templates').getOne(id); - console.log("Server template response:", response); + // console.log("Server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error(`Error fetching server 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 server template with data:", data); + // 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); + // console.log("Create server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error("Error creating server template:", error); + // console.error("Error creating server template:", error); throw error; } }, async updateTemplate(id: string, data: Partial): Promise { try { - console.log(`Updating server template with id: ${id}`, data); + // 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); + // console.log("Update server template response:", response); return response as unknown as NotificationTemplate; } catch (error) { - console.error(`Error updating server 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 server template with id: ${id}`); + // console.log(`Deleting server template with id: ${id}`); await pb.collection('server_notification_templates').delete(id); - console.log("Server template deleted successfully"); + // console.log("Server template deleted successfully"); return true; } catch (error) { - console.error(`Error deleting server template with id ${id}:`, error); + // console.error(`Error deleting server template with id ${id}:`, error); throw error; } } From 0cba09f53dfbb8715f13e1f1ece4aac84462baf0 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 22 Jul 2025 15:46:44 +0700 Subject: [PATCH 25/36] Updated Credentail for Demo [Docs]: Demo Login #74 --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2bdff25..ed8c3df 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 ### -![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-monitoring.png) -![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-service-detail.png) -![checkcle-server-instance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/instance-server-monitoring.png) +![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-1.4.png) +![Service Detail Page](https://cdn.checkcle.io/images/uptime/uptime-regional-detail.png) +![checkcle-server-instance](https://cdn.checkcle.io/images/server/server-list.png) +![SSL Monitoring](https://cdn.checkcle.io/images/ssl-domain/ssl-list.png) ![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-schedule-maintenance.png) -![SSL Monitoring](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/ssl-monitoring.png) + ## πŸ“ Development Roadmap @@ -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://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors) - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=operacle/checkcle&type=Date)](https://www.star-history.com/#operacle/checkcle&Date) - -Stay informed, stay online, with CheckCle! 🌐 From 9f704bb1adf0aad686e15e5c75deaa499c65ab48 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 22 Jul 2025 20:11:11 +0700 Subject: [PATCH 26/36] Added Reddit Community --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed8c3df..5c3664d 100644 --- a/README.md +++ b/README.md @@ -163,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) From 57f815e29ef14dc76b4750cf84cfaab2782126c8 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 23 Jul 2025 21:52:06 +0700 Subject: [PATCH 27/36] Updated server agent automate bash script --- scripts/server-agent.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/server-agent.sh b/scripts/server-agent.sh index 3435b3e..e73db52 100644 --- a/scripts/server-agent.sh +++ b/scripts/server-agent.sh @@ -232,7 +232,7 @@ configure_agent() { log_info "Final configuration:" log_info " Server Name: $SERVER_NAME" log_info " Agent ID: $AGENT_ID" - log_info " PocketBase URL: $POCKETBASE_URL" + 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" @@ -341,7 +341,7 @@ test_installation() { show_post_install_info() { echo echo "=============================================" - echo " Installation Complete!" + echo " CheckCle Server Agent Installation Complete!" echo "=============================================" echo log_success "CheckCle Monitoring Agent installed and configured successfully" From fc568891d50d3123a7b7f5070b0cdf7c9c70fdfc Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 23 Jul 2025 21:53:31 +0700 Subject: [PATCH 28/36] =?UTF-8?q?=F0=9F=93=96=20Updated=20README=20-=20For?= =?UTF-8?q?=20project=20status=20and=20url=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c3664d..d9cbe4f 100644 --- a/README.md +++ b/README.md @@ -130,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) From 4ff92b4c4da5e85609628eaa7249c98cada58af5 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 23 Jul 2025 21:55:23 +0700 Subject: [PATCH 29/36] Updated server metric field --- .../1753102275_updated_server_metrics.js | 61 +++++++++++++++++++ .../1753102296_updated_server_metrics.js | 42 +++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 server/pb_migrations/1753102275_updated_server_metrics.js create mode 100644 server/pb_migrations/1753102296_updated_server_metrics.js 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) +}) From fda91aedb93e65222d336f729fd0acc7aa76afb9 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 23 Jul 2025 22:13:20 +0700 Subject: [PATCH 30/36] Fix: Display new service immediately - Ensure that newly created services are immediately visible in the services table list on the Uptime Monitoring dashboard, without requiring a manual refresh or a delay. --- .../src/components/services/AddServiceDialog.tsx | 7 ++++++- .../components/services/add-service/ServiceForm.tsx | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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/add-service/ServiceForm.tsx b/application/src/components/services/add-service/ServiceForm.tsx index 8e66a92..28ba8e9 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({ @@ -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(); From 2bc508a23052b972a1fbe3be6dc0f9dbd1e7a88b Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 23 Jul 2025 22:55:04 +0700 Subject: [PATCH 31/36] Updated GitHub issue templates for Bug Report, Help Request, and Translation Request - Bug Report template with detailed reproduction steps, logs, and checklist - Ask for Help template to guide users in submitting clear questions - Translation Request template to facilitate multilingual contributions --- .github/ISSUE_TEMPLATE/bug_report.yml | 29 +++++++++---------- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 12 +++----- .../{ask_for_help.yml => question.yml} | 24 +++++++-------- 4 files changed, 31 insertions(+), 36 deletions(-) rename .github/ISSUE_TEMPLATE/{ask_for_help.yml => question.yml} (50%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bb41a83..2fa2743 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,16 +9,16 @@ body: attributes: value: | **Thank you for taking the time to report a bug!** - Please fill out the following template so we can reproduce and fix the issue faster. + Please complete the form below to help us reproduce and fix the issue more effectively. - type: input id: environment attributes: label: Environment description: | - Describe where you encountered the issue. - (e.g., OS, browser, Node version, etc.) - placeholder: "Example: macOS 14.0, Chrome 125, Node.js 20.3.0" + 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 @@ -27,7 +27,7 @@ body: attributes: label: Bug Description description: | - A clear and concise description of what the bug is. + A clear and concise description of the issue you encountered. placeholder: "When I do X, Y happens instead of Z..." validations: required: true @@ -37,13 +37,12 @@ body: attributes: label: Steps to Reproduce description: | - How can we reproduce the behavior? - Please list the steps in order. + Provide step-by-step instructions to reproduce the issue. placeholder: | 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' - 4. See error + 4. See the error validations: required: true @@ -61,8 +60,8 @@ body: attributes: label: Screenshots or Videos description: | - If applicable, add screenshots or screen recordings to help explain your problem. - placeholder: "Attach screenshots here." + If applicable, add screenshots or screen recordings to help illustrate the issue. + placeholder: "Drag and drop or paste screenshots here." validations: required: false @@ -71,8 +70,8 @@ body: attributes: label: Logs description: | - If applicable, paste logs or error messages here. - Please remove any sensitive information. + If applicable, paste any relevant logs or error messages. + Please remove sensitive information before submitting. render: shell placeholder: | ``` @@ -86,9 +85,9 @@ body: id: terms attributes: label: Checklist - description: Before submitting, please confirm: + description: Please confirm the following before submitting: options: - - label: I have searched existing issues to avoid duplicates. + - label: I have searched existing issues to avoid creating duplicates. required: true - - label: I have provided enough information for reproduction. + - 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 index ec4bb38..a49eab2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1 @@ -blank_issues_enabled: false \ No newline at end of file +blank_issues_enabled: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 90423a9..5c9b4e6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -15,8 +15,7 @@ body: id: description attributes: label: Feature Description - description: | - A clear and concise description of the feature you’d like to see. + description: A clear and concise description of the feature you’d like to see. placeholder: "I would like to have..." validations: required: true @@ -25,8 +24,7 @@ body: id: motivation attributes: label: Motivation - description: | - Please explain why this feature would be useful. + description: Please explain why this feature would be useful. placeholder: "This feature would help because..." validations: required: true @@ -35,8 +33,7 @@ body: id: alternatives attributes: label: Alternatives Considered - description: | - Have you considered any alternative solutions or workarounds? + description: Have you considered any alternative solutions or workarounds? placeholder: "I have tried..." validations: required: false @@ -45,8 +42,7 @@ body: id: additional attributes: label: Additional Context - description: | - Add any other context or screenshots about the feature request here. + description: Add any other context or screenshots about the feature request here. placeholder: "Links to related issues or references..." validations: required: false diff --git a/.github/ISSUE_TEMPLATE/ask_for_help.yml b/.github/ISSUE_TEMPLATE/question.yml similarity index 50% rename from .github/ISSUE_TEMPLATE/ask_for_help.yml rename to .github/ISSUE_TEMPLATE/question.yml index 54e72f7..4b20107 100644 --- a/.github/ISSUE_TEMPLATE/ask_for_help.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,5 +1,5 @@ name: ❓ Ask for Help -description: Ask a question or request guidance about this project. +description: Ask a question or request guidance related to this project. title: "[Question]: " labels: [question] assignees: [] @@ -8,8 +8,8 @@ body: - type: markdown attributes: value: | - **Thank you for reaching out!** - Please fill out this form to help us understand your question. + **Thanks for reaching out!** + Please provide as much detail as possible so we can better assist you. - type: textarea id: question @@ -17,17 +17,17 @@ body: 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... + I am trying to do X, but I'm not sure how to... validations: required: true - type: textarea id: context attributes: - label: Context + label: Additional Context description: | - Provide any additional context, related issues, or links that help explain your question. - placeholder: "Related issues, pull requests, or documentation..." + 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 @@ -36,8 +36,8 @@ body: attributes: label: Environment Details description: | - If applicable, please provide details about your environment (OS, Node version, browser, etc.). - placeholder: "Example: Ubuntu 22.04, Node.js v20, Chrome 125" + 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 @@ -45,9 +45,9 @@ body: id: checklist attributes: label: Checklist - description: Before submitting, please confirm: + description: Before submitting, please confirm the following: options: - - label: I have searched existing issues and discussions. + - label: I have searched existing issues and discussions for a solution. required: true - - label: I have read the documentation. + - label: I have read the relevant documentation. required: true From 9d5fb72708371df27476ed120c9c36db54e5332e Mon Sep 17 00:00:00 2001 From: Michael Ortner Date: Thu, 24 Jul 2025 07:59:50 +0200 Subject: [PATCH 32/36] [IMP] Added view details (for the names) on the Server Instances and SSL Views --- .../src/components/servers/ServerTable.tsx | 20 +++++++++++-------- .../ssl-domain/SSLCertificateActions.tsx | 10 +++++----- .../ssl-domain/SSLCertificatesTable.tsx | 10 +++++++--- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/application/src/components/servers/ServerTable.tsx b/application/src/components/servers/ServerTable.tsx index 89c948f..afea63c 100644 --- a/application/src/components/servers/ServerTable.tsx +++ b/application/src/components/servers/ServerTable.tsx @@ -269,7 +269,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) const isProcessing = pausingServers.has(server.id); return ( - + handleViewDetails(server.id)} + >
{server.name} @@ -325,7 +329,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) {new Date(server.last_checked).toLocaleString()}
- + e.stopPropagation()}> - handleViewDetails(server.id)}> + { e.stopPropagation(); handleViewDetails(server.id); }}> View Server Detail {server.docker === 'true' && ( - handleViewContainers(server.id)}> + { e.stopPropagation(); handleViewContainers(server.id); }}> Container Monitoring )} handlePauseResume(server)} + onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }} disabled={isProcessing} > {isPaused ? ( @@ -366,12 +370,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) )} - handleEdit(server)}> + { e.stopPropagation(); handleEdit(server); }}> Edit Server handleDelete(server)} + onClick={(e) => { e.stopPropagation(); handleDelete(server); }} className="text-red-600 focus:text-red-600" > @@ -427,4 +431,4 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) ); -}; \ No newline at end of file +}; 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 75df57a..dd28818 100644 --- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx +++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx @@ -151,7 +151,11 @@ export const SSLCertificatesTable = () => { {certificates.map((certificate) => ( - + openViewDialog(certificate)} + > {certificate.domain} @@ -170,7 +174,7 @@ export const SSLCertificatesTable = () => { {certificate.check_interval || 1} {t('days')} - + e.stopPropagation()}> {
); -}; \ No newline at end of file +}; From e9cedef443c253ecf67881c592c96d81bed677dd Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 25 Jul 2025 16:59:49 +0700 Subject: [PATCH 33/36] Disabled console log for production --- .../CreateOperationalPageDialog.tsx | 10 +++--- .../components/public/PublicStatusPage.tsx | 4 +-- .../public/hooks/usePublicStatusPageData.ts | 36 +++++++++---------- .../services/add-service/ServiceForm.tsx | 4 +-- .../services/add-service/ServiceUrlField.tsx | 4 +-- .../service-status/pauseMonitoring.ts | 2 +- .../src/services/operationalPageService.ts | 10 +++--- .../services/statusPageComponentsService.ts | 12 +++---- 8 files changed, 41 insertions(+), 41 deletions(-) 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/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/services/add-service/ServiceForm.tsx b/application/src/components/services/add-service/ServiceForm.tsx index 28ba8e9..a888b11 100644 --- a/application/src/components/services/add-service/ServiceForm.tsx +++ b/application/src/components/services/add-service/ServiceForm.tsx @@ -70,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 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/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/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/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; } } From 07dc10a4228492ab935255d90e579b0e6343ca7a Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 25 Jul 2025 17:34:37 +0700 Subject: [PATCH 34/36] Remove Quick Add Template - This section provided pre-defined component types for quick addition, but is no longer needed. --- .../operational-page/ComponentsSelector.tsx | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/application/src/components/operational-page/ComponentsSelector.tsx b/application/src/components/operational-page/ComponentsSelector.tsx index e291a9f..7f744eb 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({ @@ -206,37 +201,6 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
)} - -
- -
- {componentTypes.map((type) => { - const Icon = type.icon; - return ( - - ); - })} -
-
); From 8d279aa0dac5319cd3b6c08edc24c8ee3548a3f6 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 25 Jul 2025 17:37:29 +0700 Subject: [PATCH 35/36] Fix: Resolve invalid DOM nesting in OperationalPageCard - Resolved the `validateDOMNesting` warning. The `Badge` will be rendered directly within a `
` or other appropriate block-level element. --- .../src/components/operational-page/OperationalPageCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: From 86e707803eb02883ee092d57abf0ed02a5e7514b Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 25 Jul 2025 17:42:25 +0700 Subject: [PATCH 36/36] Fix: Improve dropdown theme support - Update dropdown themes by ensuring its background color adapts correctly. --- .../src/components/operational-page/ComponentsSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/components/operational-page/ComponentsSelector.tsx b/application/src/components/operational-page/ComponentsSelector.tsx index 7f744eb..9478fc7 100644 --- a/application/src/components/operational-page/ComponentsSelector.tsx +++ b/application/src/components/operational-page/ComponentsSelector.tsx @@ -164,7 +164,7 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC - + {services.map((service) => (