33 Commits

Author SHA1 Message Date
Tola Leng 257718e9d6 feat: Add notification, integrate data retention service, and improve i18n support (#164)
* fix(ssl):  Ensure edit form saves notification_id and template_id in DB.

- The SSL edit form was not saving the `notification_id` and `template_id` fields to the PB database when re-assigning the Notification Channel and Alert Template.

* chore(partners): add Cloudflare to Ecosystem & Community Partner section

- Cloudflare has been added to the Ecosystem & Community Partner list to acknowledge their support and contribution.

*  chore(project): update development status and progress

- fix(ssl): ensure edit form saves notification_id and template_id in DB
- feat(notifications): add Notifiarr notification channel (planned)

* feat: Add Notifiarr to notification channels (UI)

- Add Notifiarr as a new channel type in the notification settings. The Notifiarr channel will use the `api_token` field for API key storage in PB.

* feat: Add channel_id field for Notifiarr

- Add channel_id field for Notifiarr to store record of the discord channel_id into the PB.

* feat: Implement Notifiarr notification service

- The Notifiarr notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `channel_id` field for send to the Notifiarr API endpoint.

*  chore(project): update development status and progress

- feat: Add channel_id field for Notifiarr
- feat: Implement Notifiarr notification service

Closes: #133

* feat: add SSL history and Notifiarr notification channel schema

* Fix: Remove duplicate close button

- Removed the duplicate close button from the test email dialog.

* feat: Add Gotify to notification channels

- Add Gotify  as a new channel type in the notification settings. The Gotify channel will use the `api_token` and `server_url` field for API key storage in PB.

* feat: Implement Gotify notification service

- The Gotify notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `server_url` field for send to the Gotify API endpoint.

*  chore(project): update development status and progress
- feat: Add server_url field for Gotify
- feat: Implement Gotify notification service

* refactory(i18n): Improve internationalization configuration and add new translations

- Added multiple new translation items
- Updated the two language files: en and zhcn
- Added type definitions for new translation items in types/services.ts
- Modified some components to use the new translation items

* refactory(i18n): Added architecture import function translation for the "About" page

- Added translation items related to schema import in English, Chinese, and type definition files
- Replaced hard-coded text with new translation items in the AboutSystem component

* refactory(i18n): Added architecture import function translation for the "About" page

- Added translation items related to schema import in English, Chinese, and type definition files
- Replaced hard-coded text with new translation items in the AboutSystem component

* refactory(i18n): Add internationalization support for the service statistics card

* feat: Add NTFY API token field for Token-based authentication to ntfy server

- Add the api_token field for NTFY notifications in both the dialog and service to save it to PB. NTFY schema to include api_token field.

* feat(auth): add token support for ntfy

- Updated the NTFY service to support API token authentication by adding Authorization Bearer header when api_token is provided.

Closes: #89

* refactor(i18n): Added Chinese translations for Create Event and Maintenance

- Supplemented missing Chinese translations
- Added translation entries related to Create Event and Maintenance in both English and Chinese translation files

* chore: deactivate GitHub donation

* refactor(i18n): Optimize internationalization configuration and add new translation items

- Add internationalization support to the LoadingState component
- Implement internationalization translations in the Profile page
- Use internationalized text in the TestEmailDialog component
- Update English and Chinese translation files by adding new translation entries

* docs(readme): deactivate donations, accept only infra support (cloud, domain, hosting)

* feat(i18n): Enhanced internationalization support with variable interpolation

- Refactored the `t` function in LanguageContext to support multiple parameter combinations
- Added variable interpolation feature, allowing variables in translated texts
- Optimized translation logic to improve accuracy and performance
- Enhanced type safety by using TypeScript generics to ensure correct typing

* refactor(i18n): Optimize service pagination internationalization copy

- Added service pagination related copy in English and Chinese translation files
- Updated type definitions to include new translation fields
- Modified the ServicesPagination component to use the newly added translation fields

* docs(README_zhcn): Update documentation content and sponsorship policy

- Update image links in the documentation
- Revise the sponsorship policy to no longer accept individual sponsorships, only enterprise-level collaborations
- Update the display method for supporters and partners
- Add Cloudflare and DigitalOcean as new partners

* feat: Integrate data retention service

- Implemented the data retention service in Go (service-operation) that manages cleanup of old records based on configured retention periods. The service runs once per day to ensure outdated data is removed automatically.

* refactor: Remove manual cleanup buttons from Data Retention Settings

- The three manual cleanup buttons have been removed from the dashboard.
- The data retention service now automatically runs once per day, cleaning up older data based on the configured retention settings.

---------

Co-authored-by: YiZixuan <sqkkyzx@qq.com>
Co-authored-by: YiZixuan <sqkkyzx@outlook.com>
2025-09-13 21:38:38 +07:00
Tola Leng c1464202c7 Merge pull request #155 from operacle/develop
Merge pull request  from operacle/develop
2025-08-29 00:45:32 +07:00
Tola Leng 6462e5c551 Merge pull request #129 from operacle/develop
feat: Implement comprehensive powerful multi-channel notification system with templating:
- Add robust notification system supporting 7+ communication channels with intelligent message templating, resource monitoring, and SSL certificate alerts. Notification Channels: (Telegram, Discord, Slack, Signal, Email, Google Chat, Webhooks).
- This notification system provides enterprise-grade alerting capabilities with extensive customization options and multi-channel redundancy for critical service monitoring.
- Chinese language added
- Server and Service Table row clickable to detail page.
- Implement pagination for the SSL dashboard table
- Server Agent (RPM, Docker container, and general binary package) 
- Improve Uptime Service and Server connection update based on status and notification.
- Improve SSL perform the initial check automatically after creation 
- Backend Rate limiting and abuse protection
2025-08-14 23:01:14 +07:00
Tola Leng 80a2eb383d Merge pull request #112 from operacle/develop
feat: add docker compose for server docker agent and Fix: Add tar.gz fallback for unsupported systems
and other improvement
2025-07-31 04:10:06 +07:00
Tola Leng 27ee133e92 Merge pull request #84 from operacle/develop
Updated GitHub issue templates for Bug Report, Help Request,...
2025-07-23 22:56:43 +07:00
Tola Leng af64ac31ea Merge pull request #83 from operacle/develop
Fix: Display new service immediately
2025-07-23 22:29:48 +07:00
Tola Leng 72984f5da4 Merge pull request #79 from operacle/develop
feat: Add create instance monitoring agent and Add Uptime Heatmap to service detail page
2025-07-22 16:08:14 +07:00
Tola Leng cc132a66d9 Merge pull request #70 from operacle/develop
- Improve service listing to display all records correctly
- Fetch all data and handle pagination for better UX
- Improve SSL Certs listing to display all records correctly
- Added ISSUE TEMPLATE 
- Added CODE OF CONDUCT
2025-07-15 22:18:48 +07:00
Tola Leng 3f975db568 Merge pull request #68 from operacle/develop
- feat: Implement notification channels based on status (Service dialog form) and refactor Service form into smaller components
- feat: Allow multiple selections for regional agents and support multiple notifications in the Service form
- feat: Add Japanese language support. Thank you @gnsworks for the contribution!
2025-07-13 18:44:44 +07:00
Tola Leng 547a4c4041 Merge pull request #65 from operacle/develop
feat: Implement notification channel based on status(multi selection) and Refactor Split ServiceForm into smaller components.
2025-07-10 22:17:02 +07:00
Tola Leng e29f60447d Merge pull request #64 from operacle/develop
feat: Improve server overview cards and fix regional_status to Service type
2025-07-08 22:03:15 +07:00
Tola Leng ad869c17aa add codeowners and pull request workflow 2025-07-03 21:03:09 +07:00
Tola Leng 6702bfa5e3 Merge pull request #62 from operacle/develop
Fix: Instance monitoring dashboard styling and Improved docker compose configuration
2025-07-03 20:59:59 +07:00
Tola Leng cb6946b7dd Create FUNDING.yml 2025-07-03 20:56:11 +07:00
Tola Leng 8bd8ae25e6 Merge pull request #61 from operacle/develop
Fix: Server Historical Performance Metrics date filter and improved the container entrypoint script pipeline
2025-07-02 20:05:50 +07:00
Tola Leng eddc12018f Merge pull request #60 from operacle/develop
Fix: Server history chart and date filter and Added Distributed Regional Monitoring Dockerfile
2025-07-01 20:07:23 +07:00
Tola Leng 474a439515 Merge pull request #59 from operacle/develop
feat: Implement instance and docker monitoring dashboard and detail metrics page
2025-06-30 23:07:58 +07:00
Tola Leng 9a4cc7c3db Merge pull request #57 from operacle/develop
Refactor: Separate data display by monitoring source and feat: Implement "Regional Monitoring" option
2025-06-26 16:28:32 +07:00
Tola Leng 23f3c5b8ca Merge pull request #53 from operacle/develop
Integrate regional agents in service-operation and Enhance Regional Agent Installation
2025-06-21 21:49:19 +07:00
Tola Leng f88cc32cf9 Merge pull request #52 from operacle/develop
Implement SSL certificate checker in Go instead of TS and feat: Add error message and details columns
2025-06-20 20:24:55 +07:00
Tola Leng 9f4e759b01 Merge pull request #51 from operacle/develop
Implement CheckCle Microservice Operation
2025-06-19 17:51:39 +07:00
Tola Leng 9ed4d4c7ac Merge pull request #50 from operacle/develop
feat: New Create Service Form and  Split UptimeBar.tsx into smaller components
2025-06-19 15:04:07 +07:00
Tola Leng 9a001f6aec Merge pull request #49 from operacle/develop
Remove HTTP service check and Refactor: Split settings API into modules
2025-06-18 22:09:40 +07:00
Tola Leng cfdedb9636 Merge pull request #45 from operacle/develop
Fix: Implement public status page functionality
2025-06-15 20:49:51 +07:00
Tola Leng 9d5fa805c9 Merge pull request #42 from operacle/develop
Implement password reset functionality.
2025-06-12 17:15:08 +07:00
Tola Leng 68a4ad5478 Merge pull request #38 from operacle/develop
Refactor: Split Sidebar.tsx into smaller components
2025-06-08 21:46:27 +07:00
Tola Leng 9b1918e25a Merge pull request #36 from operacle/develop
feat: Implement Operational and Public Status Page Feature
2025-06-06 19:23:54 +07:00
Tola Leng 1dc25cc461 Merge pull request #35 from operacle/develop
feat: Add mail test functionality
2025-06-06 16:48:46 +07:00
Tola Leng 4dc005c7e4 Merge pull request #33 from operacle/develop
Add SMTP password field in Mail Settings and  Fix: Ensure Add Service dialog respects theme
2025-06-05 17:40:17 +07:00
Tola Leng 4ec0c7a8fa Merge pull request #32 from operacle/develop
Fix: Improve email change flow and auto-logout and feat: Implement uptime monitoring data retention
2025-06-04 16:33:42 +07:00
Tola Leng dd758b9334 Merge pull request #27 from operacle/develop
feat: Implement uptime monitoring data retention
2025-05-31 17:44:43 +07:00
Tola Leng aec7118496 Merge pull request #25 from operacle/develop
feat: Auto logout after password change
2025-05-28 15:37:53 +07:00
Tola Leng 759c3249e1 Merge pull request #22 from operacle/develop
feat: release maintenance and incident management features; apply hotfixes
2025-05-26 21:57:45 +07:00
27 changed files with 579 additions and 1422 deletions
+1 -2
View File
@@ -85,8 +85,7 @@ body:
id: terms
attributes:
label: Checklist
description: |
Please confirm the following before submitting:
description: Please confirm the following before submitting:
options:
- label: I have searched existing issues to avoid creating duplicates.
required: true
+1 -2
View File
@@ -51,8 +51,7 @@ body:
id: checklist
attributes:
label: Checklist
description: |
Before submitting, please confirm:
description: Before submitting, please confirm:
options:
- label: I have searched existing issues to make sure this feature hasnt been requested yet.
required: true
+1 -2
View File
@@ -45,8 +45,7 @@ body:
id: checklist
attributes:
label: Checklist
description: |
Before submitting, please confirm the following:
description: Before submitting, please confirm the following:
options:
- label: I have searched existing issues and discussions for a solution.
required: true
+19 -23
View File
@@ -1,29 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle</title>
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle - Dev</title>
<meta name="description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-184
View File
@@ -8,7 +8,6 @@
"name": "vite_react_shadcn_ts",
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -645,21 +644,6 @@
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -679,26 +663,6 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
@@ -2316,103 +2280,6 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@react-aria/focus": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.5",
"@react-aria/utils": "^3.30.1",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.25.5",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.30.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.10.8",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -2862,15 +2729,6 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@swc/types": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
@@ -2935,33 +2793,6 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -6458,12 +6289,6 @@
"node": ">=12.0.0"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@@ -6743,15 +6568,6 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-2
View File
@@ -11,7 +11,6 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -46,7 +45,6 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"framer-motion": "12.23.12",
"input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
@@ -154,8 +154,7 @@
"email",
"ntfy",
"pushover",
"notifiarr",
"gotify"
"notifiarr"
]
},
{
@@ -340,20 +339,6 @@
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3401535476",
"max": 0,
"min": 0,
"name": "server_url",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
+43 -137
View File
@@ -1,40 +1,37 @@
import React, { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/sonner";
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@/components/ui/sonner';
import { ThemeProvider } from "./contexts/ThemeContext";
import { LanguageProvider } from "./contexts/LanguageContext";
import { SidebarProvider } from "./contexts/SidebarContext";
import { ThemeProvider } from './contexts/ThemeContext';
import { LanguageProvider } from './contexts/LanguageContext';
import { SidebarProvider } from './contexts/SidebarContext';
import Index from "./pages/Index";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import InstanceMonitoring from "./pages/InstanceMonitoring";
import ContainerMonitoring from "./pages/ContainerMonitoring";
import ServiceDetail from "./pages/ServiceDetail";
import SslDomain from "./pages/SslDomain";
import ScheduleIncident from "./pages/ScheduleIncident";
import OperationalPage from "./pages/OperationalPage";
import RegionalMonitoring from "./pages/RegionalMonitoring";
import Settings from "./pages/Settings";
import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
import PublicStatusPage from "./pages/PublicStatusPage";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import ServerDetail from "./pages/ServerDetail";
import Index from './pages/Index';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import InstanceMonitoring from './pages/InstanceMonitoring';
import ContainerMonitoring from './pages/ContainerMonitoring';
import ServiceDetail from './pages/ServiceDetail';
import SslDomain from './pages/SslDomain';
import ScheduleIncident from './pages/ScheduleIncident';
import OperationalPage from './pages/OperationalPage';
import RegionalMonitoring from './pages/RegionalMonitoring';
import Settings from './pages/Settings';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';
import PublicStatusPage from './pages/PublicStatusPage';
import { ProtectedRoute } from './components/auth/ProtectedRoute';
import ServerDetail from './pages/ServerDetail';
function App() {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
);
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000
}
}
}));
return (
<BrowserRouter>
@@ -49,110 +46,19 @@ function App() {
{/* Protected routes */}
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<Index />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/instance-monitoring"
element={
<ProtectedRoute>
<InstanceMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/server-detail/:serverId"
element={
<ProtectedRoute>
<ServerDetail />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring/:serverId"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route
path="/regional-monitoring"
element={
<ProtectedRoute>
<RegionalMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
<Route path="/service/:id" element={<ProtectedRoute><ServiceDetail /></ProtectedRoute>} />
<Route path="/ssl-domain" element={<ProtectedRoute><SslDomain /></ProtectedRoute>} />
<Route path="/schedule-incident" element={<ProtectedRoute><ScheduleIncident /></ProtectedRoute>} />
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="*" element={<NotFound />} />
</Routes>
</QueryClientProvider>
@@ -1,3 +1,4 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@@ -14,24 +15,17 @@ interface DashboardContentProps {
error: Error | null;
}
export const DashboardContent = ({
services,
isLoading,
error,
}: DashboardContentProps) => {
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
const { t } = useLanguage();
const [filter, setFilter] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>("");
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
// Filter services based on search term and type filter
const filteredServices = services.filter((service) => {
const matchesSearch =
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url &&
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter =
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
const filteredServices = services.filter(service => {
const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
return matchesSearch && matchesFilter;
});
@@ -39,23 +33,21 @@ export const DashboardContent = ({
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading service data.</p>
<Button onClick={() => window.location.reload()}>{t("retry")}</Button>
<Button onClick={() => window.location.reload()}>{t('retry')}</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl ">
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">
{t("overview")}
</h2>
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t("newService")}
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
</Button>
</div>
+33 -184
View File
@@ -1,39 +1,13 @@
import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext";
import {
Moon,
PanelLeft,
PanelLeftClose,
Sun,
Globe,
FileText,
Github,
Twitter,
MessageSquare,
Bell,
User,
Settings,
LogOut,
CaseUpper,
BookA,
CaseSensitive,
CaseLower,
ALargeSmall,
} from "lucide-react";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import useDynamicFontSize from "@/contexts/FontSizeContext";
interface HeaderProps {
currentUser: AuthUser | null;
@@ -46,7 +20,7 @@ export const Header = ({
currentUser,
onLogout,
sidebarCollapsed,
toggleSidebar,
toggleSidebar
}: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage();
@@ -81,35 +55,27 @@ export const Header = ({
}, [currentUser]);
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
let avatarUrl = "";
let avatarUrl = '';
if (currentUser?.avatar) {
// If it's a relative path from the public folder, make sure it's resolved properly
if (currentUser.avatar.startsWith("/upload/profile/")) {
if (currentUser.avatar.startsWith('/upload/profile/')) {
avatarUrl = currentUser.avatar;
} else {
avatarUrl = currentUser.avatar;
}
console.log("Final avatar URL:", avatarUrl);
}
const [fontSize, setFontSize] = useState(() => {
const savedFontSize = localStorage.getItem("fontSize");
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
});
useDynamicFontSize(fontSize);
return (
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
{/* Grid Pattern Overlay - Similar to StatusCards */}
<div className="absolute inset-0 z-0">
<div
className="w-full h-full dark:bg-black"
className="w-full h-full"
style={{
backgroundImage: `linear-gradient(${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px),
linear-gradient(90deg, ${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
>
<div className="w-full h-full backdrop-blur-[1px]"></div>
@@ -118,100 +84,45 @@ export const Header = ({
{/* Header Content */}
<div className="flex items-center gap-4 z-10">
<div
className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${
!sidebarCollapsed ? "hidden" : ""
}`}
>
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className={`h-6 w-6 `}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="mr-2"
>
{sidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</Button>
<div className="flex items-center space-x-2">
<h1 className="font-medium text-xl">
{greeting},{" "}
{currentUser?.name || currentUser?.email?.split("@")[0] || "User"}{" "}
👋
</h1>
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
</div>
</div>
<div className="flex items-center space-x-4 z-10">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={toggleTheme}
>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}>
<span className="sr-only">Toggle theme</span>
{theme === "dark" ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<span className="sr-only">{t("language")}</span>
<Globe className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setLanguage("en")}
className={language === "en" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
{t("english")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("km")}
className={language === "km" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
{t("khmer")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("de")}
className={language === "de" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
{t("Deutsch")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("ko")}
className={language === "ko" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
{t("korean")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("ja")}
className={language === "ja" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
{t("japanese")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("zhcn")}
className={language === "zhcn" ? "bg-accent" : ""}
>
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
{t("simplifiedChinese")}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -233,9 +144,7 @@ export const Header = ({
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() =>
window.open("https://github.com/operacle/checkcle", "_blank")
}
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
>
<span className="sr-only">GitHub</span>
<Github className="w-4 h-4" />
@@ -264,87 +173,30 @@ export const Header = ({
</Button>
{/* Notifications */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" />
</Button>
{/* Fond Size */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<BookA className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev + 1)}
className={`cursor-pointer`}
>
<span className={`text-[15px] font-semibold`}> Aa +</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev - 1)}
className={`cursor-pointer`}
>
<span className={`text-[12px] font-semibold`}> Aa -</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* User Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-3 p-2">
<Avatar className="h-10 w-10">
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
</Avatar>
<div className="flex flex-col space-y-1">
<span className="font-medium">
{currentUser?.name || "User"}
</span>
<span className="text-xs text-muted-foreground truncate">
{currentUser?.email}
</span>
<span className="font-medium">{currentUser?.name || 'User'}</span>
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
</div>
</div>
<DropdownMenuSeparator />
@@ -357,10 +209,7 @@ export const Header = ({
<span>{t("settings")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onLogout}
className="text-red-500 focus:text-red-500"
>
<DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500">
<LogOut className="mr-2 h-4 w-4" />
<span>{t("logout")}</span>
</DropdownMenuItem>
@@ -1,12 +1,7 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
@@ -23,26 +18,24 @@ export const ServiceFilters = ({
setFilter,
searchTerm,
setSearchTerm,
servicesCount,
servicesCount
}: ServiceFiltersProps) => {
const { t } = useLanguage();
return (
<div className="mb-6 flex justify-between items-center ">
<div className="mb-6 flex justify-between items-center">
<div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground">
{t("currentlyMonitoring")}
</h3>
<h3 className="text-xl font-semibold mr-2 text-foreground">{t('currentlyMonitoring')}</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount}
</span>
</div>
<div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
<SelectValue placeholder={t("allTypes")} />
<SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder={t('allTypes')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("allTypes")}</SelectItem>
<SelectItem value="all">{t('allTypes')}</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem>
@@ -52,7 +45,7 @@ export const ServiceFilters = ({
<div className="relative">
<Input
className="w-72 bg-card border-border"
placeholder={t("search")}
placeholder={t('search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -1,3 +1,4 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader";
@@ -14,21 +15,14 @@ export const Sidebar = ({ collapsed }: SidebarProps) => {
return (
<div
className={`
${
theme === "dark"
? "bg-black border-[#1e1e1e]"
: "bg-sidebars border-sidebar-border"
}
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
border-r flex flex-col h-full
${collapsed ? "w-16" : "w-64"}
${collapsed ? "hidden" : ""}`}
${collapsed ? 'w-16' : 'w-64'}
`}
>
<SidebarHeader collapsed={collapsed} />
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} />
<div className="items-center ml-2 p-2 h-30 border-t ">
Version: latest
</div>
</div>
);
};
@@ -1,3 +1,4 @@
import React from "react";
import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData";
@@ -6,15 +7,9 @@ interface MainNavigationProps {
collapsed: boolean;
}
export const MainNavigation: React.FC<MainNavigationProps> = ({
collapsed,
}) => {
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
return (
<nav
className={`my-2 mx-1 py-1 px-1 dark:bg-black ${
collapsed ? "hidden" : ""
}`}
>
<nav className="my-2 mx-1 py-1 px-1">
{mainMenuItems.map((item) => (
<MenuItem
key={item.id}
@@ -1,3 +1,4 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
@@ -21,7 +22,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
translationKey,
color,
hasNavigation,
collapsed,
collapsed
}) => {
const { theme } = useTheme();
const { t } = useLanguage();
@@ -43,24 +44,18 @@ export const MenuItem: React.FC<MenuItemProps> = ({
return (
<div
className={`
${collapsed ? "p-3" : "p-2 pl-3"}
${collapsed ? 'p-3' : 'p-2 pl-3'}
mb-1 rounded-lg
${
isActive
? theme === "dark"
? "bg-drak"
: "bg-sidebar-accent "
: `hover:${theme === "dark" ? "bg-gray-800" : "bg-sidebar-accent "}`
}
${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
flex items-center
${collapsed ? "justify-center" : ""}
${collapsed ? 'justify-center' : ''}
cursor-pointer
`}
onClick={handleClick}
>
<Icon className={`${mainIconSize} ${color}`} />
{!collapsed && (
<span className="ml-2.5 font-medium text-foreground tracking-wide ">
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
{t(translationKey)}
</span>
)}
@@ -1,13 +1,10 @@
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { Settings, ChevronDown } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { settingsMenuItems } from "./navigationData";
@@ -20,27 +17,19 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
const { t } = useLanguage();
const location = useLocation();
const navigate = useNavigate();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
"general"
);
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
const savedValue = localStorage.getItem("settingsPanelOpen");
// Check if the saved value exists and is "true"
return savedValue === "true";
});
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
// Update active settings item based on URL
useEffect(() => {
if (location.pathname === "/settings") {
if (location.pathname === '/settings') {
const params = new URLSearchParams(location.search);
const panel = params.get("panel");
const panel = params.get('panel');
if (panel) {
setActiveSettingsItem(panel);
}
}
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
}, [location, settingsPanelOpen]);
}, [location]);
const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item);
@@ -54,27 +43,15 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
};
const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${
isActive
? theme === "dark"
? "bg-[#1a1a1a]"
: "bg-sidebar-accent"
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
} rounded-lg flex items-center `;
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
};
if (collapsed) {
const mainIconSize = "h-6 w-6";
return (
<div
className={`border-t ${
theme === "dark"
? "border-[#1e1e1e] bg-[#121212]"
: "border-sidebar-border bg-sidebar"
} p-4 flex justify-center ${collapsed ? "hidden" : ""}`}
>
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<div
onClick={(e) => handleMenuItemClick("/settings", e)}
onClick={(e) => handleMenuItemClick('/settings', e)}
className="cursor-pointer"
>
<Settings className={`${mainIconSize} text-purple-400`} />
@@ -84,59 +61,33 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
}
return (
<div
className={`flex-1 flex flex-col border-t ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-sidebars"
} p-4 ${collapsed ? "hidden" : ""}`}
>
<Collapsible
open={settingsPanelOpen}
onOpenChange={setSettingsPanelOpen}
className="w-full flex flex-col flex-1"
>
<CollapsibleTrigger
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
}`}
>
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
<div className="flex items-center">
<span className="font-medium tracking-wide">
{t("settingPanel")}
</span>
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
</div>
<div className="flex items-center">
<Settings className="h-4 w-4 mr-1" />
<ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${
settingsPanelOpen ? "rotate-180" : ""
}`}
/>
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent
className={`${
theme === "dark" ? "bg-black" : "bg-sidebars"
} flex-1 flex flex-col`}
>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
<ScrollArea className="h-full">
<div className="space-y-2 pr-4 cursor-pointer">
<div className="space-y-2 pr-4">
{settingsMenuItems.map((item) => (
<div
key={item.id}
className={getMenuItemClasses(
activeSettingsItem === item.id
)}
className={getMenuItemClasses(activeSettingsItem === item.id)}
onClick={(e) => {
handleMenuItemClick(`/settings?panel=${item.id}`, e);
handleSettingsItemClick(item.id);
}}
>
<item.icon className="h-4 w-4 mr-2" />
<span className="text-base">{t(item.translationKey)}</span>
<span className="text-sm">{t(item.translationKey)}</span>
</div>
))}
</div>
@@ -1,3 +1,4 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
@@ -9,15 +10,13 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme();
return (
<div
className={`p-[15.7px] ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-white"
} border-b flex items-center ${collapsed ? "justify-center" : ""}`}
>
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
<div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2">
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className="h-6 w-6"
/>
</div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div>
@@ -1,17 +1,14 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
useOperationalPages,
useDeleteOperationalPage,
} from "@/hooks/useOperationalPage";
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
import { OperationalPageCard } from "./OperationalPageCard";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { OperationalPageRecord } from "@/types/operational.types";
import { Activity, Plus, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
import { EditOperationalPageDialog } from './EditOperationalPageDialog';
import { OperationalPageCard } from './OperationalPageCard';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { OperationalPageRecord } from '@/types/operational.types';
import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import {
AlertDialog,
AlertDialogAction,
@@ -21,26 +18,17 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
} from '@/components/ui/alert-dialog';
export const OperationalPageContent = () => {
const navigate = useNavigate();
const {
data: pages,
isLoading,
error,
refetch,
isRefetching,
} = useOperationalPages();
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
const deleteMutation = useDeleteOperationalPage();
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(
null
);
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pageToDelete, setPageToDelete] =
useState<OperationalPageRecord | null>(null);
const [pageToDelete, setPageToDelete] = useState<OperationalPageRecord | null>(null);
const handleEdit = (page: OperationalPageRecord) => {
setEditingPage(page);
@@ -49,10 +37,10 @@ export const OperationalPageContent = () => {
const handleView = (page: OperationalPageRecord) => {
if (page.custom_domain) {
window.open(`https://${page.custom_domain}`, "_blank");
window.open(`https://${page.custom_domain}`, '_blank');
} else {
// Navigate to the public status page route using the correct format
window.open(`/public/${page.slug}`, "_blank");
window.open(`/public/${page.slug}`, '_blank');
}
};
@@ -68,7 +56,7 @@ export const OperationalPageContent = () => {
setDeleteDialogOpen(false);
setPageToDelete(null);
} catch (error) {
console.error("Error deleting page:", error);
console.error('Error deleting page:', error);
}
}
};
@@ -80,9 +68,7 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">
Failed to load operational pages
</h3>
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
<p className="text-muted-foreground mb-4">
There was an error loading your operational pages. Please try again.
</p>
@@ -96,13 +82,11 @@ export const OperationalPageContent = () => {
}
return (
<div className="w-full container mx-auto px-4 py-8 ">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8 ">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">
Operational Pages
</h1>
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
<p className="text-muted-foreground">
Manage your public status pages and monitor service health
</p>
@@ -115,9 +99,7 @@ export const OperationalPageContent = () => {
onClick={() => refetch()}
disabled={isRefetching}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
/>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
Refresh
</Button>
<CreateOperationalPageDialog />
@@ -153,12 +135,9 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">
No operational pages found
</h3>
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
<p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your
services and communicate status to your users.
Create your first operational page to start monitoring your services and communicate status to your users.
</p>
<CreateOperationalPageDialog />
</CardContent>
@@ -188,12 +167,12 @@ export const OperationalPageContent = () => {
<span className="font-medium">Total Pages:</span> {pages.length}
</div>
<div>
<span className="font-medium">Public Pages:</span>{" "}
{pages.filter((p) => p.is_public === "true").length}
<span className="font-medium">Public Pages:</span>{' '}
{pages.filter(p => p.is_public === 'true').length}
</div>
<div>
<span className="font-medium">Operational:</span>{" "}
{pages.filter((p) => p.status === "operational").length}
<span className="font-medium">Operational:</span>{' '}
{pages.filter(p => p.status === 'operational').length}
</div>
</div>
</div>
@@ -212,9 +191,7 @@ export const OperationalPageContent = () => {
<AlertDialogHeader>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{pageToDelete?.title}"? This
action cannot be undone and will permanently remove the
operational page and all its components.
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -224,7 +201,7 @@ export const OperationalPageContent = () => {
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting..." : "Delete"}
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -1,13 +1,8 @@
import React, { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { RegionalService } from "@/types/regional.types";
@@ -20,29 +15,24 @@ export const RegionalMonitoringContent = () => {
const { toast } = useToast();
const queryClient = useQueryClient();
const {
data: regionalServices = [],
isLoading,
error,
} = useQuery({
queryKey: ["regional-services"],
const { data: regionalServices = [], isLoading, error } = useQuery({
queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices,
refetchInterval: 30000, // Refetch every 30 seconds
});
const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: "Regional Agent Added",
description:
"The regional monitoring agent has been successfully configured.",
description: "The regional monitoring agent has been successfully configured.",
});
};
const handleDeleteAgent = async (id: string) => {
try {
await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: "Agent Removed",
description: "The regional monitoring agent has been removed.",
@@ -56,18 +46,14 @@ export const RegionalMonitoringContent = () => {
}
};
const onlineAgents = regionalServices.filter(
(agent) => agent.connection === "online"
).length;
const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length;
const totalAgents = regionalServices.length;
return (
<div className="p-6 space-y-6 ">
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Regional Monitoring
</h1>
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
<p className="text-muted-foreground">
Manage distributed monitoring agents across different regions
</p>
@@ -99,25 +85,23 @@ export const RegionalMonitoringContent = () => {
<Wifi className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Currently connected</p>
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Currently connected
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Offline Agents
</CardTitle>
<CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
<WifiOff className="h-4 w-4 text-red-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{totalAgents - onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Disconnected agents</p>
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Disconnected agents
</p>
</CardContent>
</Card>
</div>
@@ -149,8 +133,7 @@ export const RegionalMonitoringContent = () => {
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<p className="text-muted-foreground text-center mb-4">
Get started by adding your first regional monitoring agent to
extend your monitoring coverage.
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
</p>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
@@ -1,37 +1,33 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
import { IncidentManagementTab } from "./IncidentManagementTab";
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog";
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog";
import { useToast } from "@/hooks/use-toast";
import {
initMaintenanceNotifications,
stopMaintenanceNotifications,
} from "@/services/maintenance/maintenanceNotificationService";
import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
import { useToast } from '@/hooks/use-toast';
import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
export const ScheduleIncidentContent = () => {
const { t } = useLanguage();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("maintenance");
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
useState(false);
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
// Initialize maintenance notifications when the component mounts
useEffect(() => {
// console.log("Initializing maintenance notifications");
// console.log("Initializing maintenance notifications");
initMaintenanceNotifications();
// Clean up when the component unmounts
return () => {
// console.log("Cleaning up maintenance notifications");
// console.log("Cleaning up maintenance notifications");
stopMaintenanceNotifications();
};
}, []);
@@ -47,44 +43,42 @@ export const ScheduleIncidentContent = () => {
const handleMaintenanceCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = refreshTrigger + 1;
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
setRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t("success"),
description: t("maintenanceCreatedSuccess"),
title: t('success'),
description: t('maintenanceCreatedSuccess'),
});
};
const handleIncidentCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = incidentRefreshTrigger + 1;
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
setIncidentRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t("success"),
description: t("incidentCreatedSuccess"),
title: t('success'),
description: t('incidentCreatedSuccess'),
});
};
return (
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
<div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl">
<main className="flex-1 flex flex-col overflow-auto bg-background p-6">
<div className="flex flex-col gap-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-foreground">
{t("scheduleIncidentManagement")}
{t('scheduleIncidentManagement')}
</h2>
<Button
className="text-primary-foreground"
onClick={handleCreateButtonClick}
>
<Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance"
? t("createMaintenanceWindow")
: t("createIncident")}
{activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
</Button>
</div>
@@ -96,11 +90,11 @@ export const ScheduleIncidentContent = () => {
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="maintenance">
<CalendarClock className="w-4 h-4 mr-2" />
{t("scheduledMaintenance")}
{t('scheduledMaintenance')}
</TabsTrigger>
<TabsTrigger value="incidents">
<AlertCircle className="w-4 h-4 mr-2" />
{t("incidentManagement")}
{t('incidentManagement')}
</TabsTrigger>
</TabsList>
@@ -1,45 +1,13 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
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 {
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 { 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";
@@ -55,11 +23,7 @@ interface ServerTableProps {
onRefresh: () => void;
}
export const ServerTable = ({
servers,
isLoading,
onRefresh,
}: ServerTableProps) => {
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
const { theme } = useTheme();
const [searchTerm, setSearchTerm] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -70,11 +34,10 @@ export const ServerTable = ({
const navigate = useNavigate();
const { toast } = useToast();
const filteredServers = servers.filter(
(server) =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
const filteredServers = servers.filter(server =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleViewDetails = (serverId: string) => {
@@ -94,38 +57,35 @@ export const ServerTable = ({
}
try {
setPausingServers((prev) => new Set(prev).add(serverId));
setPausingServers(prev => new Set(prev).add(serverId));
// Only update the status field, preserving all other server configuration
const updateData = {
status: isPaused ? "up" : "paused",
last_checked: new Date().toISOString(),
last_checked: new Date().toISOString()
};
await pb.collection("servers").update(serverId, updateData);
await pb.collection('servers').update(serverId, updateData);
toast({
title: isPaused ? "Server resumed" : "Server paused",
description: `Monitoring ${isPaused ? "resumed" : "paused"} for ${
server.name
}`,
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
});
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
// 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",
description: `Failed to ${
isPaused ? "resume" : "pause"
} server monitoring. Please try again.`,
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
});
} finally {
setPausingServers((prev) => {
setPausingServers(prev => {
const newSet = new Set(prev);
newSet.delete(serverId);
return newSet;
@@ -150,7 +110,7 @@ export const ServerTable = ({
setIsDeleting(true);
// Delete the server from the database
await pb.collection("servers").delete(selectedServer.id);
await pb.collection('servers').delete(selectedServer.id);
toast({
title: "Server deleted",
@@ -163,8 +123,9 @@ export const ServerTable = ({
// Close the dialog
setDeleteDialogOpen(false);
setSelectedServer(null);
} catch (error) {
// console.error('Error deleting server:', error);
// console.error('Error deleting server:', error);
toast({
variant: "destructive",
title: "Error",
@@ -179,61 +140,61 @@ export const ServerTable = ({
value,
label,
subtitle,
type,
type
}: {
value: number;
label: string;
subtitle: string;
type: "cpu" | "memory" | "disk";
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 === '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 === '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";
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";
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";
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 (
<div className="space-y-2 min-w-[120px] -mt-20">
<div className="space-y-2 min-w-[120px]">
<div className="flex justify-between items-center">
<span className={`text-sm font-semibold ${getTextColor(value)}`}>
{label}
</span>
<span className="text-xs text-muted-foreground">{subtitle} </span>
<span className="text-xs text-muted-foreground">
{subtitle}
</span>
</div>
<div className="relative">
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
<div
className={`h-full bg-gradient-to-r ${getGradientColors(
type,
value
)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
className={`h-full bg-gradient-to-r ${getGradientColors(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
style={{ width: `${Math.min(value, 100)}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse opacity-60" />
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
</div>
</div>
</div>
</div>
);
@@ -241,7 +202,7 @@ export const ServerTable = ({
if (isLoading) {
return (
<Card className="flex-1 flex flex-col ">
<Card className="flex-1 flex flex-col">
<CardHeader className="flex-shrink-0">
<CardTitle>Servers</CardTitle>
</CardHeader>
@@ -283,107 +244,27 @@ export const ServerTable = ({
<p className="text-muted-foreground">No servers found</p>
</div>
) : (
<div
className={`${
theme === "dark" ? "bg-gray-900" : "bg-white"
} rounded-lg border border-border shadow-sm`}
>
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
<Table>
<TableHeader
className={`${
theme === "dark" ? "bg-gray-800" : "bg-gray-50"
}`}
>
<TableRow
className={`${
theme === "dark"
? "border-gray-700 hover:bg-gray-800"
: "border-gray-200 hover:bg-gray-100"
}`}
>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Name
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Status
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
OS
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
IP Address
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
CPU
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Memory
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Disk
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Uptime
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Last Checked
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4 text-right`}
>
Actions
</TableHead>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Status</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>OS</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>IP Address</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Last Checked</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{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 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);
@@ -404,10 +285,7 @@ export const ServerTable = ({
<TableCell>
<div className="flex items-center gap-2">
<OSTypeIcon osType={server.os_type} />
<span
className="text-sm truncate"
title={server.os_type}
>
<span className="text-sm truncate" title={server.os_type}>
{server.os_type}
</span>
</div>
@@ -429,9 +307,7 @@ export const ServerTable = ({
<CustomProgressBar
value={memoryUsage}
label={`${memoryUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(
server.ram_total
)}
subtitle={serverService.formatBytes(server.ram_total)}
type="memory"
/>
</TableCell>
@@ -439,17 +315,12 @@ export const ServerTable = ({
<CustomProgressBar
value={diskUsage}
label={`${diskUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(
server.disk_total
)}
subtitle={serverService.formatBytes(server.disk_total)}
type="disk"
/>
</TableCell>
<TableCell>
<div
className="text-sm truncate"
title={server.uptime}
>
<div className="text-sm truncate" title={server.uptime}>
{server.uptime}
</div>
</TableCell>
@@ -458,17 +329,10 @@ export const ServerTable = ({
{new Date(server.last_checked).toLocaleString()}
</div>
</TableCell>
<TableCell
className="text-right"
onClick={(e) => e.stopPropagation()}
>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isProcessing}
>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
<span className="sr-only">Open menu</span>
{isProcessing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
@@ -477,36 +341,20 @@ export const ServerTable = ({
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-[200px]"
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleViewDetails(server.id);
}}
>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
<Eye className="mr-2 h-4 w-4" />
View Server Detail
</DropdownMenuItem>
{server.docker === "true" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleViewContainers(server.id);
}}
>
{server.docker === 'true' && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
<Activity className="mr-2 h-4 w-4" />
Container Monitoring
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePauseResume(server);
}}
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
disabled={isProcessing}
>
{isPaused ? (
@@ -522,20 +370,12 @@ export const ServerTable = ({
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleEdit(server);
}}
>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
<Edit className="mr-2 h-4 w-4" />
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(server);
}}
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -566,19 +406,19 @@ export const ServerTable = ({
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to delete this server?
</AlertDialogTitle>
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete{" "}
This action cannot be undone. This will permanently delete{' '}
<span className="font-semibold text-foreground">
{selectedServer?.name}
</span>{" "}
</span>{' '}
and all of its monitoring data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isDeleting}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isDeleting}
@@ -25,21 +25,19 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
return start;
}, [endDate, interval]);
// Fetch uptime data with immediate display and background updates
// 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: 300000, // Data stays fresh for 5 minutes - display immediately from cache
gcTime: 900000, // Keep in cache for 15 minutes
refetchInterval: 300000, // Reduced to 5 minute polling
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: 1, // Reduce retries for faster failure
retryDelay: 1000, // Faster retry delay
refetchIntervalInBackground: true, // Allow background updates
placeholderData: (previousData) => previousData, // Keep showing previous data while refetching
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Memoize the uptime calculation to prevent unnecessary recalculations
@@ -2,24 +2,13 @@ import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
import { SSLCertificatesTable } from "./SSLCertificatesTable";
import { LoadingState } from "@/components/services/LoadingState";
import {
fetchSSLCertificates,
addSSLCertificate,
checkAndUpdateCertificate,
refreshAllCertificates,
deleteSSLCertificate,
} from "@/services/ssl";
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
@@ -32,23 +21,18 @@ export const SSLDomainContent = () => {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
const [selectedCertificate, setSelectedCertificate] =
useState<SSLCertificate | null>(null);
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
const queryClient = useQueryClient();
// Fetch SSL certificates with explicit error handling
const {
data: certificates = [],
isLoading,
error,
} = useQuery({
queryKey: ["ssl-certificates"],
const { data: certificates = [], isLoading, error } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: async () => {
try {
const result = await fetchSSLCertificates();
return result;
} catch (error) {
toast.error(t("failedToLoadCertificates"));
toast.error(t('failedToLoadCertificates'));
throw error;
}
},
@@ -60,34 +44,32 @@ export const SSLDomainContent = () => {
const addMutation = useMutation({
mutationFn: addSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsAddDialogOpen(false);
toast.success(t("sslCertificateAdded"));
toast.success(t('sslCertificateAdded'));
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : t("failedToAddCertificate")
);
},
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
}
});
// Edit certificate mutation - Updated to include notification_id and template_id
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
// Create the update data object with new fields
const updateData = {
warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel,
notification_id: certificate.notification_id || "", // Multi notification channels
template_id: certificate.template_id || "", // Alert template ID
notification_id: certificate.notification_id || '', // Multi notification channels
template_id: certificate.template_id || '', // Alert template ID
check_interval: certificate.check_interval,
};
// Update certificate in the database using PocketBase directly
const updated = await pb
.collection("ssl_certificates")
.update(certificate.id, updateData);
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
// 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
@@ -96,37 +78,33 @@ export const SSLDomainContent = () => {
return refreshedCert;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsEditDialogOpen(false);
setSelectedCertificate(null);
toast.success(t("sslCertificateUpdated"));
toast.success(t('sslCertificateUpdated'));
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : t("failedToUpdateCertificate")
);
},
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
});
// Delete certificate mutation
const deleteMutation = useMutation({
mutationFn: deleteSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
toast.success(t("sslCertificateDeleted"));
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
toast.success(t('sslCertificateDeleted'));
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : t("failedToDeleteCertificate")
);
},
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
});
// Refresh certificate mutation - Updated to remove individual toast notifications
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null);
// Removed individual success toast notification
},
@@ -134,40 +112,33 @@ export const SSLDomainContent = () => {
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
// Removed individual error toast notification
},
}
});
// Refresh all certificates mutation
const refreshAllMutation = useMutation({
mutationFn: refreshAllCertificates,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsRefreshingAll(false);
if (result.failed === 0) {
toast.success(
t("allCertificatesRefreshed").replace(
"{count}",
result.success.toString()
)
);
toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
} else {
toast.info(
t("someCertificatesFailed")
.replace("{success}", result.success.toString())
.replace("{failed}", result.failed.toString())
);
toast.info(t('someCertificatesFailed')
.replace('{success}', result.success.toString())
.replace('{failed}', result.failed.toString()));
}
},
onError: (error) => {
toast.error(t("failedToCheckCertificate"));
toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false);
// Still refresh the data to show any partial information
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
},
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
});
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
@@ -195,14 +166,12 @@ export const SSLDomainContent = () => {
const handleRefreshAll = async () => {
if (certificates.length === 0) {
toast.info(t("noCertificatesToRefresh"));
toast.info(t('noCertificatesToRefresh'));
return;
}
setIsRefreshingAll(true);
toast.info(
t("startingRefreshAll").replace("{count}", certificates.length.toString())
);
toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
refreshAllMutation.mutate();
};
@@ -213,29 +182,21 @@ export const SSLDomainContent = () => {
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>{t("failedToLoadCertificates")}</p>
<Button
onClick={() =>
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
}
>
{t("check")}
<p>{t('failedToLoadCertificates')}</p>
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
{t('check')}
</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background pb-0 dark:bg-black p-2 ">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-6 rounded-xl">
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-foreground">
{t("sslDomainManagement")}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t("monitorSSLCertificates")}
</p>
<h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
</div>
<div className="flex gap-2">
<Button
@@ -244,12 +205,8 @@ export const SSLDomainContent = () => {
disabled={isRefreshingAll || refreshingId !== null}
className="relative"
>
<RefreshCw
className={`w-4 h-4 mr-2 ${
isRefreshingAll ? "animate-spin" : ""
}`}
/>
{t("refreshAll")}
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
{t('refreshAll')}
{isRefreshingAll && (
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
...
@@ -260,7 +217,7 @@ export const SSLDomainContent = () => {
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t("addDomain")}
<Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
</Button>
</div>
</div>
@@ -275,7 +232,7 @@ export const SSLDomainContent = () => {
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("addSSLCertificate")}</DialogTitle>
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
@@ -286,16 +243,13 @@ export const SSLDomainContent = () => {
</Dialog>
{selectedCertificate && (
<Dialog
open={isEditDialogOpen}
onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}
>
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("editSSLCertificate")}</DialogTitle>
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
</DialogHeader>
<EditSSLCertificateForm
certificate={selectedCertificate}
@@ -1,14 +0,0 @@
// src/contexts/FontSizeContext.js
import { useEffect } from "react";
const useDynamicFontSize = (size) => {
useEffect(() => {
if (typeof size === "number") {
// Apply font size to the root HTML element
document.documentElement.style.fontSize = `${size}px`;
localStorage.setItem("fontSize", size.toString());
}
}, [size]);
};
export default useDynamicFontSize;
+14 -24
View File
@@ -1,48 +1,38 @@
import React, { createContext, useContext, useState, useEffect } from "react";
type SideBar = boolean;
import React, { createContext, useContext, useState } from 'react';
interface SidebarContextType {
sidebarCollapsed: SideBar;
setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>;
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebar: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
interface SidebarProviderProps {
children: React.ReactNode;
}
export const SidebarProvider: React.FC<SidebarProviderProps> = ({
children,
}) => {
// Initialize the state
const [sidebarCollapsed, setSidebarCollapsed] = useState<SideBar>(() => {
const saved = localStorage.getItem("sidebarCollapsed");
return saved === "true";
});
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => {
setSidebarCollapsed((prev) => {
const newState = !prev;
localStorage.setItem("sidebarCollapsed", newState.toString());
return newState;
});
setSidebarCollapsed(prev => !prev);
};
const value = {
sidebarCollapsed,
setSidebarCollapsed,
toggleSidebar,
toggleSidebar
};
return (
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
);
};
// A custom hook to consume the sidebar context.
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (context === undefined) {
throw new Error("useSidebar must be used within a SidebarProvider");
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
+18 -32
View File
@@ -1,3 +1,4 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useTheme } from "@/contexts/ThemeContext";
@@ -25,21 +26,16 @@ const InstanceMonitoring = () => {
total: 0,
online: 0,
offline: 0,
warning: 0,
warning: 0
});
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
const [addDialogOpen, setAddDialogOpen] = useState(false);
const {
data: servers = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["servers"],
const { data: servers = [], isLoading, error, refetch } = useQuery({
queryKey: ['servers'],
queryFn: serverService.getServers,
refetchInterval: 30000, // Refetch every 30 seconds
refetchInterval: 30000 // Refetch every 30 seconds
});
useEffect(() => {
@@ -54,7 +50,7 @@ const InstanceMonitoring = () => {
const handleLogout = () => {
authService.logout();
navigate("/login");
navigate('/login');
};
const handleAgentAdded = () => {
@@ -72,14 +68,11 @@ const InstanceMonitoring = () => {
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6 ">
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
<div className="text-center max-w-md w-full">
<h2 className="text-xl sm:text-2xl font-bold mb-4">
Error loading servers
</h2>
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2>
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
Unable to fetch server data. Please check your connection and
try again.
Unable to fetch server data. Please check your connection and try again.
</p>
<button
onClick={handleRefresh}
@@ -95,20 +88,20 @@ const InstanceMonitoring = () => {
}
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground ">
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0 ">
<div className="flex flex-col flex-1 min-w-0">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 overflow-auto dark:bg-black p-2 shadow">
<div className="p-4 sm:p-2 lg:p-2 space-y-6 h-full bg-white dark:bg-black p-4 rounded-xl">
<main className="flex-1 overflow-auto">
<div className="p-4 sm:p-6 lg:p-8 space-y-6">
{/* Header Section */}
<div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-2 ">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="min-w-0 flex-1">
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
Instance Monitoring
@@ -117,10 +110,7 @@ const InstanceMonitoring = () => {
Monitor and manage your server instances in real-time
</p>
</div>
<Button
onClick={() => setAddDialogOpen(true)}
className="flex-shrink-0"
>
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
<Plus className="mr-2 h-4 w-4" />
Add Server Agent
</Button>
@@ -128,17 +118,13 @@ const InstanceMonitoring = () => {
</div>
{/* Stats Cards Section */}
<div className="p-2 ">
<div>
<ServerStatsCards stats={stats} />
</div>
{/* Server Table Section */}
<div className="p-3">
<ServerTable
servers={servers}
isLoading={isLoading}
onRefresh={handleRefresh}
/>
<div>
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
</div>
</div>
</main>
+14 -24
View File
@@ -1,3 +1,4 @@
import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { authService } from "@/services/authService";
@@ -6,11 +7,7 @@ import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
import { LoadingState } from "@/components/services/LoadingState";
import {
fetchSSLCertificates,
shouldRunDailyCheck,
checkAllCertificatesAndNotify,
} from "@/services/sslCertificateService";
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
import { useLanguage } from "@/contexts/LanguageContext";
import { useSidebar } from "@/contexts/SidebarContext";
@@ -26,21 +23,16 @@ const SslDomain = () => {
const navigate = useNavigate();
// Fetch SSL certificates with error handling and debugging
const {
data: certificates = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["ssl-certificates"],
const { data: certificates = [], isLoading, error, refetch } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: async () => {
// console.log("Fetching SSL certificates from SslDomain page...");
// console.log("Fetching SSL certificates from SslDomain page...");
try {
const result = await fetchSSLCertificates();
// console.log("SSL certificates fetch successful, count:", result.length);
// console.log("SSL certificates fetch successful, count:", result.length);
return result;
} catch (err) {
// console.error("Error fetching SSL certificates from page:", err);
// console.error("Error fetching SSL certificates from page:", err);
throw err;
}
},
@@ -54,7 +46,7 @@ const SslDomain = () => {
const checkCertificates = async () => {
// Check if we should run daily check
if (shouldRunDailyCheck()) {
// console.log("Running daily SSL certificate check...");
// console.log("Running daily SSL certificate check...");
await checkAllCertificatesAndNotify();
// Refresh certificate list after daily check
refetch();
@@ -94,25 +86,23 @@ const SslDomain = () => {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 ">
<div className="flex flex-col flex-1">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<div className="flex flex-col items-center justify-center h-full p-6 ">
<h2 className="text-xl font-bold mb-2">
{t("failedToLoadCertificates")}
</h2>
<div className="flex flex-col items-center justify-center h-full p-6">
<h2 className="text-xl font-bold mb-2">{t('failedToLoadCertificates')}</h2>
<p className="text-muted-foreground mb-4">
{error instanceof Error ? error.message : t("unknown")}
{error instanceof Error ? error.message : t('unknown')}
</p>
<button
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
onClick={() => refetch()}
>
{t("check")}
{t('check')}
</button>
</div>
</div>
@@ -124,7 +114,7 @@ const SslDomain = () => {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 ">
<div className="flex flex-col flex-1">
<Header
currentUser={currentUser}
onLogout={handleLogout}
+2 -9
View File
@@ -76,7 +76,7 @@ The roadmap is divided into the following stages:
- [ ] ✅ Add ntfy push notifications
### 🚀 v1.6.0 Feature Enhancements _(Target: Q3 2025)_
**Status:** ✅ Completed
**Status:** 🔧 In Progress
#### Tentative Features:
- [ ] ✅ Allow user to update the schema directly from the dashboard
- [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143)
@@ -89,14 +89,7 @@ The roadmap is divided into the following stages:
- [ ] ✅ fix(ssl): Ensure edit form saves notification_id and template_id in DB.
- [ ] ✅ Add Notifiarr notifications
- [ ] ✅ Add Gotify notifications
- [ ] Enhance the Data retention feature
### 🚀 v1.7.0 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress
#### Tentative Features:
- [ ] 🔧 Fix for the response time dashboard filter (date/time) in the uptime detail page
- [ ] 🔧 Fix the server historical loading data
- [ ] 🔧 Enhance the Data retention feature
- [ ] 🔧 Improve docker agent for server monitoring
- [ ] 🔧 Improve the Server Agent on Unraid
- [ ] 🔧 Server Windows Agent