74 Commits

Author SHA1 Message Date
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 7a00d8828e 💼 Update on Project Development Status and Progress 2025-08-14 22:56:52 +07:00
Tola Leng 6193ad8b4f 📧 Added Powerful Notification System Image and Sponsorship 2025-08-14 22:54:02 +07:00
Tola Leng 41c387a301 Removed the insert Webhook Payload Template in the Notification Channel Dialog. 2025-08-14 21:00:15 +07:00
Tola Leng 933e70a3f6 feat: Implement comprehensive multi-channel notification system with templating and SSL monitoring
- 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.
2025-08-14 20:50:31 +07:00
Tola Leng 58242c33f8 Refactor Webhook to use alert_configurations and Add Signal API endpoint field
- This field will be used to specify the API endpoint for Signal notifications.
- This change aligns the frontend with the backend's data structure for webhooks.
2025-08-14 04:54:15 +07:00
Tola Leng 47a38d3c64 Refactor Webhook fields
- Condense webhook fields in the notification channel form to display only essential information, mirroring the approach in webhook service.
2025-08-14 03:39:47 +07:00
Tola Leng 07c101617c feat: Add SMTP password field for email
- Add an input field for SMTP password in the "Add Notification Channel" dialog. This field will be used for SMTP authentication.
2025-08-13 23:08:10 +07:00
Tola Leng 37bb7e25b6 Fix: Display existing SSL notification and template data
- This change ensures that users can see and manage their existing SSL configurations.
2025-08-13 21:42:48 +07:00
Tola Leng a4e819d220 Fix: Preserve notification IDs when disabling notifications
- Allowing notifications to be re-enabled correctly without losing previously configured
2025-08-13 17:20:24 +07:00
Tola Leng 7b01cffb64 Merge pull request #128 from sqkkyzx/develop
feat(translations): Add Simplified Chinese translation
2025-08-13 14:42:18 +07:00
YiZixuan 6479ea50a5 docs: Add Chinese documentation and update the language selection list
- Add a README_zhcn.md file in the docs directory, which contains the Chinese documentation content.
- Add a link to the Chinese language option in README.md and READMEs in other languages
2025-08-13 00:48:37 +08:00
YiZixuan 9596f53742 feat(translations): Add German and Japanese translation fields
- Added the "german" and "japanese" fields to the common.ts file
2025-08-13 00:22:44 +08:00
YiZixuan f15a00772e feat(translations): Add Simplified Chinese translation
- Add simplified Chinese translation files
- Add the Simplified Chinese option in the language selection
- Update the translation index to support Simplified Chinese
2025-08-13 00:05:16 +08:00
Tola Leng d4636d5436 feat: Add error_message placeholder for uptime notifications
- Add support for the `${error_message}` placeholder in uptime service notifications
2025-08-12 22:26:51 +07:00
Tola Leng 37bc86e9d8 feat: Enhance uptime notification templates
- Improve Telegram notification messages for uptime services by adding support for new placeholders.
2025-08-12 22:23:31 +07:00
Tola Leng 34d3e81154 Removed entrypoint script with hidden pocketbase 2025-08-12 15:12:35 +07:00
Tola Leng e70b23cbe5 Add placeholders to Service Uptime templates
- Adds support for new placeholders in the Service Uptime Monitoring alert templates. The following placeholders are now available:
-- ${ip_host}: The IP address or hostname of the service.
-- ${service_type}: The type of the service (e.g., HTTP, PING, TCP, DNS).
-- ${port}: The port the service is running on.
-- ${domain}: The domain name of the service.
-- ${region_agent}: The regional agent monitoring the service.
2025-08-12 14:49:13 +07:00
Tola Leng 36dfd8a52c Add restore messages to server notification templates
- Add new fields for restore messages to the Server Template Dialog to match the DB schema.
2025-08-11 23:09:43 +07:00
Tola Leng 86503b7160 Fix: Prevent clearing notification fields
- This fix ensures that when the notification status is toggled off, notification_channels, threshold_id, and template_id are no longer cleared or emptied.
2025-08-11 17:43:55 +07:00
Tola Leng aecf16ba40 Fix: Correctly update notification status
- This fix ensures that when the notification status is enabled. Resolves the issue where the status was incorrectly shown as false in PB.
2025-08-11 17:33:08 +07:00
Tola Leng 2ca7c35c69 Update Translate SSL Certificate Details
- Translate untranslated text in the SSL Certificate Details dialog. The following keys have been translated:
- certificateDetails
- subjectAlternativeNames
- monitoringConfiguration
- technicalInformation
2025-08-10 16:38:15 +07:00
Tola Leng 81604cb736 Merge pull request #119 from gnsworks/develop
Update Japanese translation
2025-08-10 16:21:29 +07:00
Tola Leng 9f9d773256 Fix: Wrap Subject Alternative Names
- Wrap the Subject Alternative Names (SANs) in the SSL detail dialog to ensure they break onto new lines when there are multiple entries, improving readability.
2025-08-10 16:07:04 +07:00
Tola Leng 92e5f3f074 feat: Add Server Threshold Tab
- Add a new tab for "Server Threshold" to the Alert Templates dashboard, to allow users to manage server threshold configurations.
2025-08-09 20:06:40 +07:00
gnsworks 2f12f8f805 Add docs/DEVELOPMENT_ROADMAP_ja.md 2025-08-09 13:16:22 +09:00
gnsworks c6e7e243e6 Update : japanese documents translation 2025-08-09 11:57:14 +09:00
gnsworks 074fc18ee1 Merge branch 'operacle:develop' into develop 2025-08-09 08:54:50 +09:00
Tola Leng 34dccb763d 📝 Added sponsoring the OSS documentation site 2025-08-08 21:24:39 +07:00
Tola Leng cc6abf58ac Remove Quick Actions button and dialog
- Remove the QuickActionsDialog component and its associated file from the dashboard.
2025-08-08 15:39:57 +07:00
Tola Leng 269240c6c9 Add icons to notification channels
- Display icons for each notification channel type in the dialog and dropdown.
2025-08-05 22:27:37 +07:00
Tola Leng 7856aff98e Updated Server Installation Tips 2025-08-05 22:01:20 +07:00
Tola Leng 99d67deb10 Add Google Chat webhook to notification channels
- Added Google Chat  as a new channel type option in the Notification Channel
- Add Google Chat tab to notifications
- Fix: Ensure Google Chat and Email save record to PB
2025-08-05 21:35:56 +07:00
Tola Leng 96bf456338 feat: Add & Enhance Notification Channel and Alert Template to Add/Edit Domain Form
- Add multi-select for notification channels and an alert template field to the Add/Edit SSL Certificate dialog
2025-08-04 21:59:04 +07:00
Tola Leng f4cc96733c Refactor threshold update logic
- Remove the separate "Update Thresholds" button and integrate its functionality into the main "Update Server" button.
2025-08-04 21:26:19 +07:00
Tola Leng da4678570f Fixed: Remove [Default] alert template option
- The Alert Template field in the Service dialog will now correctly fetch data from service_notification_templates
2025-08-04 21:14:52 +07:00
gnsworks 881d745776 Merge branch 'operacle:develop' into develop 2025-08-03 16:03:21 +09:00
Tola Leng b4dc510611 Add webhook payload template
- Add webhook notification channel configuration. Allow users to define custom payload structures for webhook notifications.
2025-08-02 20:37:51 +07:00
Tola Leng 914f1aba60 feat: Support multiple notification channels
- Update the Notification Settings dashboard to allow users to select multiple notification channels for alerts.
- Add webhook to notification channels: Add webhook as a selectable option in the notification channel type dropdown.
- Implement template type selection in the Add Template dialog. Based on the selected type.
2025-08-02 16:53:15 +07:00
Tola Leng f7cf2fcc20 Fix: Set SSL initialize check with the current time. Closes: #100
- When adding a new SSL certificate, initialize with the current time. This ensures that the SSL check is triggered shortly after the certificate is added to the monitoring system.
2025-07-31 17:28:41 +07:00
Tola Leng 50b4d1dfb0 Refactor: Reduce verbose logging output in Regional Script
- Reduce verbose logging in install-regional-agent.sh to provide a more minimal output while retaining all core functionality.
2025-07-31 16:19:16 +07:00
Tola Leng 8bb78e68ea Disable verbose logging
- Disable log console output in file to prevent logs from being printed to the console.
2025-07-31 14:57:16 +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 205bedfba5 Updated project development status 2025-07-31 02:58:02 +07:00
Tola Leng 1fac88dcc9 feat: add docker compose for server docker agent 2025-07-31 02:52:57 +07:00
Tola Leng 44bc2bc1e5 feat: Create server-docker-agent.sh script
- This script will automate the process of running the monitoring agent Docker container. It will take environment variables as input and construct the `docker run` command with the provided values, including volumes and ports, to ensure a seamless one-click installation experience.
2025-07-31 02:46:27 +07:00
Tola Leng bc06a24adb feat: Add Docker One-Click Install Tab
- Adds a new "Docker One-Click" tab to the server dashboard and create new server dialog. This tab provides a one-click installation script for the Docker monitoring agent, dynamically populating it with server-specific details.
2025-07-31 02:44:51 +07:00
Tola Leng 952eb3d8d7 Update script URL to deploy from CDN 2025-07-29 21:19:38 +07:00
Tola Leng 1d8b5172c5 Added the OS logo icon for Arch Linux 2025-07-29 20:54:54 +07:00
Tola Leng ff4938533c Fix: Add tar.gz fallback for unsupported systems
- The script has been updated to include a fallback mechanism for systems that do not have `dpkg` or `rpm` package managers. This ensures that the agent can be installed on a wider range of Linux distributions, such as Arch Linux, by automatically using the `tar.gz` package when native package managers are not detected.
2025-07-29 20:54:20 +07:00
gnsworks 384920e952 Merge branch 'operacle:develop' into develop 2025-07-29 09:03:10 +09:00
Tola Leng cad092bc6b 📖 Add sections for Project Status and Sponsorship to the README. 2025-07-28 02:12:49 +07:00
Tola Leng be57688a5e 📖 Update X (twitter) Account 2025-07-28 01:27:29 +07:00
Tola Leng e2f41fdef1 feat: Add pagination to SSL table
- Implement pagination for the SSL dashboard table, includes adding a dropdown for selecting the number of records per page (10, 30, 50).
2025-07-27 20:07:59 +07:00
Tola Leng c329425ac1 Refactor the project developement roadmap 2025-07-27 19:22:03 +07:00
Tola Leng fd3b959966 📖 Updated README - Refactor the project developement roadmap 2025-07-27 19:21:32 +07:00
gnsworks 4157ee11cc Merge branch 'operacle:develop' into develop 2025-07-25 21:20:34 +09:00
Tola Leng 86e707803e Fix: Improve dropdown theme support
- Update dropdown themes by ensuring its background color adapts correctly.
2025-07-25 17:42:25 +07:00
Tola Leng 8d279aa0da Fix: Resolve invalid DOM nesting in OperationalPageCard
- Resolved the `validateDOMNesting` warning. The `Badge` will be rendered directly within a `<div>` or other appropriate block-level element.
2025-07-25 17:37:29 +07:00
Tola Leng 07dc10a422 Remove Quick Add Template
- This section provided pre-defined component types for quick addition, but is no longer needed.
2025-07-25 17:34:37 +07:00
Tola Leng e9cedef443 Disabled console log for production 2025-07-25 16:59:49 +07:00
Tola Leng c827151521 Merge pull request #91 from michaelortnerit/develop
[IMP] Added view details (for the names) on the Server Instances and …
2025-07-25 01:31:20 +07:00
Michael Ortner 9d5fb72708 [IMP] Added view details (for the names) on the Server Instances and SSL Views 2025-07-24 07:59:50 +02: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 2bc508a230 Updated GitHub issue templates for Bug Report, Help Request, and Translation Request
- Bug Report template with detailed reproduction steps, logs, and checklist
- Ask for Help template to guide users in submitting clear questions
- Translation Request template to facilitate multilingual contributions
2025-07-23 22:55:04 +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 fda91aedb9 Fix: Display new service immediately
- Ensure that newly created services are immediately visible in the services table list on the Uptime Monitoring dashboard, without requiring a manual refresh or a delay.
2025-07-23 22:13:20 +07:00
Tola Leng 4ff92b4c4d Updated server metric field 2025-07-23 21:55:23 +07:00
Tola Leng fc568891d5 📖 Updated README - For project status and url image 2025-07-23 21:53:31 +07:00
Tola Leng 57f815e29e Updated server agent automate bash script 2025-07-23 21:52:06 +07:00
Tola Leng 9f704bb1ad Added Reddit Community 2025-07-22 20:11:11 +07:00
Tola Leng 0cba09f53d Updated Credentail for Demo [Docs]: Demo Login #74 2025-07-22 15:46:44 +07:00
gnsworks 88b4803f52 Merge branch 'operacle:develop' into develop 2025-07-15 21:23:16 +09:00
gnsworks c6ed816c04 Update Japanese docs 2025-07-14 20:32:08 +09:00
152 changed files with 12882 additions and 2123 deletions
+14 -15
View File
@@ -9,16 +9,16 @@ body:
attributes:
value: |
**Thank you for taking the time to report a bug!**
Please fill out the following template so we can reproduce and fix the issue faster.
Please complete the form below to help us reproduce and fix the issue more effectively.
- type: input
id: environment
attributes:
label: Environment
description: |
Describe where you encountered the issue.
(e.g., OS, browser, Node version, etc.)
placeholder: "Example: macOS 14.0, Chrome 125, Node.js 20.3.0"
Describe the environment where the issue occurred.
(e.g., OS, browser, Node.js version, Docker version, etc.)
placeholder: "Example: Ubuntu 22.04, Chrome 125, Node.js 20.3.0"
validations:
required: true
@@ -27,7 +27,7 @@ body:
attributes:
label: Bug Description
description: |
A clear and concise description of what the bug is.
A clear and concise description of the issue you encountered.
placeholder: "When I do X, Y happens instead of Z..."
validations:
required: true
@@ -37,13 +37,12 @@ body:
attributes:
label: Steps to Reproduce
description: |
How can we reproduce the behavior?
Please list the steps in order.
Provide step-by-step instructions to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
4. See the error
validations:
required: true
@@ -61,8 +60,8 @@ body:
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or screen recordings to help explain your problem.
placeholder: "Attach screenshots here."
If applicable, add screenshots or screen recordings to help illustrate the issue.
placeholder: "Drag and drop or paste screenshots here."
validations:
required: false
@@ -71,8 +70,8 @@ body:
attributes:
label: Logs
description: |
If applicable, paste logs or error messages here.
Please remove any sensitive information.
If applicable, paste any relevant logs or error messages.
Please remove sensitive information before submitting.
render: shell
placeholder: |
```
@@ -86,9 +85,9 @@ body:
id: terms
attributes:
label: Checklist
description: Before submitting, please confirm:
description: Please confirm the following before submitting:
options:
- label: I have searched existing issues to avoid duplicates.
- label: I have searched existing issues to avoid creating duplicates.
required: true
- label: I have provided enough information for reproduction.
- label: I have provided sufficient information to reproduce the issue.
required: true
+1 -1
View File
@@ -1 +1 @@
blank_issues_enabled: false
blank_issues_enabled: true
+4 -8
View File
@@ -15,8 +15,7 @@ body:
id: description
attributes:
label: Feature Description
description: |
A clear and concise description of the feature youd like to see.
description: A clear and concise description of the feature youd like to see.
placeholder: "I would like to have..."
validations:
required: true
@@ -25,8 +24,7 @@ body:
id: motivation
attributes:
label: Motivation
description: |
Please explain why this feature would be useful.
description: Please explain why this feature would be useful.
placeholder: "This feature would help because..."
validations:
required: true
@@ -35,8 +33,7 @@ body:
id: alternatives
attributes:
label: Alternatives Considered
description: |
Have you considered any alternative solutions or workarounds?
description: Have you considered any alternative solutions or workarounds?
placeholder: "I have tried..."
validations:
required: false
@@ -45,8 +42,7 @@ body:
id: additional
attributes:
label: Additional Context
description: |
Add any other context or screenshots about the feature request here.
description: Add any other context or screenshots about the feature request here.
placeholder: "Links to related issues or references..."
validations:
required: false
@@ -1,5 +1,5 @@
name: ❓ Ask for Help
description: Ask a question or request guidance about this project.
description: Ask a question or request guidance related to this project.
title: "[Question]: "
labels: [question]
assignees: []
@@ -8,8 +8,8 @@ body:
- type: markdown
attributes:
value: |
**Thank you for reaching out!**
Please fill out this form to help us understand your question.
**Thanks for reaching out!**
Please provide as much detail as possible so we can better assist you.
- type: textarea
id: question
@@ -17,17 +17,17 @@ body:
label: What do you need help with?
description: Clearly describe your question or what you're trying to achieve.
placeholder: |
I am trying to do X but I'm not sure how to...
I am trying to do X, but I'm not sure how to...
validations:
required: true
- type: textarea
id: context
attributes:
label: Context
label: Additional Context
description: |
Provide any additional context, related issues, or links that help explain your question.
placeholder: "Related issues, pull requests, or documentation..."
Provide any relevant context, related issues, pull requests, or documentation links that could help us understand your question better.
placeholder: "Mention related issues, links to docs, or previous discussions..."
validations:
required: false
@@ -36,8 +36,8 @@ body:
attributes:
label: Environment Details
description: |
If applicable, please provide details about your environment (OS, Node version, browser, etc.).
placeholder: "Example: Ubuntu 22.04, Node.js v20, Chrome 125"
If relevant, provide details about your environment (e.g., OS, browser, Node.js version).
placeholder: "Example: Windows 11, Node.js v20.3.0, Firefox 125"
validations:
required: false
@@ -45,9 +45,9 @@ body:
id: checklist
attributes:
label: Checklist
description: Before submitting, please confirm:
description: Before submitting, please confirm the following:
options:
- label: I have searched existing issues and discussions.
- label: I have searched existing issues and discussions for a solution.
required: true
- label: I have read the documentation.
- label: I have read the relevant documentation.
required: true
+70 -62
View File
@@ -20,6 +20,12 @@
<br/><strong>Japanese</strong>
</a>
</td>
<td align="center">
<a href="docs/README_zhcn.md">
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
<br/><strong>Chinese</strong>
</a>
</td>
</tr>
</table>
@@ -39,10 +45,12 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
## 🎯 Live Demo
👉 **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io)
User: admin@example.com | Passwd: Admin123456
## 🌟 Core Features
### 📝 Roadmap : [DEVELOPMENT_ROADMAP](docs/DEVELOPMENT_ROADMAP.md)
### Uptime Services & Infrastructure Server Monitoring
### Uptime Services & Infrastructure Server Monitoring
- Monitor HTTP, DNS, and Ping protocols
- Monitor TCP-based, API services (e.g., FTP, SMTP, HTTP)
- Track detail uptime, response times, and performance issues
@@ -62,26 +70,9 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
* ✅ x86_64 PCs, laptops, servers (amd64)
* ✅ Modern Raspberry Pi 3/4/5 with (64-bit OS), Apple Silicon Macs (arm64)
### Install CheckCle using one of the options below.
### Install CheckCle using one of the following methods:
1. CheckCle One-Click Installation - Just copy and run on terminal
```bash
curl -fsSL https://checkcle.io/install.sh | bash
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Install with Docker compose Configuration.
1. Install with Docker Compose Configuration (Recommended)
```bash
version: '3.9'
@@ -101,6 +92,18 @@ services:
hard: 8192
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Admin Web Management
Default URL: http://0.0.0.0:8090
@@ -110,33 +113,12 @@ services:
4. Follow the Quick Start Guide at https://docs.checkcle.io
###
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-monitoring.png)
![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-service-detail.png)
![checkcle-server-instance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/instance-server-monitoring.png)
![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-schedule-maintenance.png)
![SSL Monitoring](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/ssl-monitoring.png)
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-1.4.png)
![Service Detail Page](https://cdn.checkcle.io/images/uptime/uptime-regional-detail.png)
![checkcle-server-instance](https://cdn.checkcle.io/images/server/server-list.png)
![SSL Monitoring](https://cdn.checkcle.io/images/ssl-domain/ssl-list.png)
![Notification System](https://cdn.checkcle.io/general/powerfull_notification.png)
## 📝 Development Roadmap
- ✅ Health check & uptime monitoring (HTTP)
- ✅ Dashboard UI with live stats
- ✅ Auth with Multi-users system (admin)
- ✅ Notifications (Telegram)
- ✅ Docker containerization
- ✅ CheckCle Website
- ✅ CheckCle Demo Server
- ✅ SSL & Domain Monitoring
- ✅ Schedule Maintenance
- ✅ Incident Management
- [ ] Infrastructure Server Monitoring
- ✅ Operational Status / Public Status Pages
- ✅ Uptime monitoring (HTTP, TCP, PING, DNS) Full functionality
- ✅ Distributed Regional Monitoring Agent [Support Operation](https://github.com/operacle/Distributed-Regional-Monitoring)
- ✅ System Setting Panel and Mail Settings
- ✅ User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
- ✅ Data Retention & Automate Strink (Muti Options to Shrink Data & Database )
- ✅ Open-source release with full documentation
## 🌟 CheckCle for Communities?
- **Built with Passion**: Created by an open-source enthusiast for the community
@@ -145,6 +127,44 @@ services:
---
## Sponsors
🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
### 🥈 Silver Uptime Ally
<a href="https://github.com/michaelortnerit">
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
</a>
### 🧡 Ping Supporter
<a href="https://github.com/samang-dauth">
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
</a>
### 🤝 Ecosystem & Community Partner
<a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75"
style="border-radius: 50%;" />
</a>
<a href="https://m.do.co/c/0c27ef82475f" style="display: inline-block; vertical-align: middle;">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg"
height="45"
alt="DigitalOcean Logo">
</a>
---
## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
---
## 🤝 Ways to Contribute
Here are some ways you can help improve CheckCle:
@@ -153,30 +173,18 @@ Here are some ways you can help improve CheckCle:
- 🌟 **Suggest Features** Have an idea? Start a [Discussion](https://github.com/operacle/checkcle/discussions) or open a Feature Request issue.
- 🛠 **Submit Pull Requests** Improve the code, fix bugs, add features, or enhance the docs.
- 📝 **Improve Documentation** Even a typo fix helps!
- 🌍 **Spread the Word** Star ⭐ the repo, share it on socials, and invite others to contribute!
- 🌍 **Spread the Word** Star ⭐ the [CheckCle](https://github.com/operacle/checkcle.git) repo, share it on socials, and invite others to contribute!
---
## 🌍 Stay Connected
- Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- Community Channels: Engage via discussions and issues!
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
## 📜 License
CheckCle is released under the MIT License.
---
## 👥 Contributors
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=operacle/checkcle&type=Date)](https://www.star-history.com/#operacle/checkcle&Date)
Stay informed, stay online, with CheckCle! 🌐
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle is an open-source monitoring stack</title>
<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" />
@@ -14,7 +14,7 @@
<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="@tlengoss" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@@ -1,15 +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, Grid3x3 } 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 { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import QuickActionsDialog from "./QuickActionsDialog";
interface HeaderProps {
currentUser: AuthUser | null;
@@ -29,7 +27,6 @@ export const Header = ({
const [greeting, setGreeting] = useState<string>("");
const { systemName } = useSystemSettings();
const navigate = useNavigate();
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
// Set greeting based on time of day
useEffect(() => {
@@ -53,7 +50,7 @@ export const Header = ({
// Log avatar data for debugging
useEffect(() => {
if (currentUser) {
// console.log("Avatar URL in Header:", currentUser.avatar);
//console.log("Avatar URL in Header:", currentUser.avatar);
}
}, [currentUser]);
@@ -66,7 +63,7 @@ export const Header = ({
} else {
avatarUrl = currentUser.avatar;
}
// console.log("Final avatar URL:", avatarUrl);
console.log("Final avatar URL:", avatarUrl);
}
return (
@@ -91,16 +88,6 @@ export const Header = ({
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</Button>
{/* Quick Actions Button */}
<Button
variant="ghost"
size="icon"
onClick={() => setQuickActionsOpen(true)}
className="mr-2"
>
<Grid3x3 className="h-5 w-5 text-green-500" />
</Button>
<div className="flex items-center space-x-2">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
</div>
@@ -132,6 +119,9 @@ export const Header = ({
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
{t("japanese")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
{t("simplifiedChinese")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -162,7 +152,7 @@ export const Header = ({
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/tlengoss", "_blank")}
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
@@ -223,9 +213,6 @@ export const Header = ({
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Quick Actions Dialog */}
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
</header>
);
};
};
@@ -1,145 +0,0 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogClose
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface QuickActionsDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
const { t } = useLanguage();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
<DialogClose className="absolute right-4 top-4">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogClose>
<DialogHeader>
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
<DialogDescription>
{t('quickActionsDescription')}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
{/* Website Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorWebsiteDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorMultipleEndpoints')}</li>
<li>{t('trackResponseTimes')}</li>
<li>{t('setCustomAlerts')}</li>
</ul>
</div>
</div>
{/* Server Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorServerDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorCPURam')}</li>
<li>{t('trackNetworkMetrics')}</li>
<li>{t('viewProcesses')}</li>
</ul>
</div>
</div>
{/* SSL Certificate Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('sslCertificateDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('trackSSLExpiration')}</li>
<li>{t('getEarlyRenewal')}</li>
<li>{t('monitorMultipleDomains')}</li>
</ul>
</div>
</div>
{/* Incidents Management Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('incidentsManagementDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createTrackIncidents')}</li>
<li>{t('assignIncidents')}</li>
<li>{t('monitorResolution')}</li>
</ul>
</div>
</div>
{/* Operational Status Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('operationalStatusDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createCustomStatus')}</li>
<li>{t('displayRealTime')}</li>
<li>{t('shareIncidentUpdates')}</li>
</ul>
</div>
</div>
{/* Reports & Analytics Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('reportsAnalyticsDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('generateUptimeReports')}</li>
<li>{t('analyzePerformance')}</li>
<li>{t('exportMonitoringData')}</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button variant="outline" onClick={() => setIsOpen(false)}>
{t('close')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default QuickActionsDialog;
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -6,7 +7,7 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react';
import { Plus, X, Server } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
@@ -17,12 +18,6 @@ interface ComponentsSelectorProps {
onComponentDelete?: (componentId: string) => void;
}
const componentTypes = [
{ value: 'uptime', label: 'Uptime Service', icon: Server },
{ value: 'ssl', label: 'SSL Certificate', icon: Shield },
{ value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle },
];
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
@@ -169,7 +164,7 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<SelectTrigger>
<SelectValue placeholder="Select an uptime service" />
</SelectTrigger>
<SelectContent className="z-50 bg-white border shadow-lg">
<SelectContent>
{services.map((service) => (
<SelectItem key={service.id} value={service.id}>
<div className="flex items-center gap-2">
@@ -206,37 +201,6 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</div>
</div>
)}
<div className="mt-4">
<Label>Quick Add Templates</Label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-2">
{componentTypes.map((type) => {
const Icon = type.icon;
return (
<Button
key={type.value}
variant="outline"
size="sm"
onClick={() => {
const component: Partial<StatusPageComponentRecord> = {
name: type.label,
description: `Monitor ${type.label.toLowerCase()}`,
service_id: '',
server_id: '',
display_order: selectedComponents.length + 1,
operational_status_id: '',
};
onComponentsChange([...selectedComponents, component]);
}}
className="justify-start"
>
<Icon className="h-4 w-4 mr-2" />
{type.label}
</Button>
);
})}
</div>
</div>
</CardContent>
</Card>
);
@@ -67,13 +67,13 @@ export const CreateOperationalPageDialog = () => {
page_style: data.page_style || '',
};
console.log('Creating operational page with payload:', payload);
// console.log('Creating operational page with payload:', payload);
const createdPage = await createMutation.mutateAsync(payload);
console.log('Created page:', createdPage);
// console.log('Created page:', createdPage);
// Create components after page is created
if (selectedComponents.length > 0) {
console.log('Creating components for page:', createdPage.id);
// console.log('Creating components for page:', createdPage.id);
for (const component of selectedComponents) {
const componentPayload = {
operational_status_id: createdPage.id,
@@ -84,7 +84,7 @@ export const CreateOperationalPageDialog = () => {
display_order: component.display_order || 1,
};
console.log('Creating component with payload:', componentPayload);
// console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
}
@@ -93,7 +93,7 @@ export const CreateOperationalPageDialog = () => {
form.reset();
setSelectedComponents([]);
} catch (error) {
console.error('Error creating operational page:', error);
// console.error('Error creating operational page:', error);
}
};
@@ -41,11 +41,11 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
</div>
<div>
<span className="font-medium text-muted-foreground">Public:</span>
<p className="mt-1">
<div className="mt-1">
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
{page.is_public === 'true' ? 'Yes' : 'No'}
</Badge>
</p>
</div>
</div>
<div>
<span className="font-medium text-muted-foreground">Updated:</span>
@@ -12,7 +12,7 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
export const PublicStatusPage = () => {
const { slug } = useParams<{ slug: string }>();
console.log('PublicStatusPage - slug from params:', slug);
// console.log('PublicStatusPage - slug from params:', slug);
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
const [lastUpdated, setLastUpdated] = useState(new Date());
@@ -51,7 +51,7 @@ export const PublicStatusPage = () => {
};
}, [page?.theme]);
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
// console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
if (loading) {
return (
@@ -19,61 +19,61 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
useEffect(() => {
const fetchPublicPage = async () => {
if (!slug) {
console.log('No slug provided');
// console.log('No slug provided');
setError('No status page slug provided');
setLoading(false);
return;
}
try {
console.log('Fetching public status page for slug:', slug);
// console.log('Fetching public status page for slug:', slug);
setLoading(true);
setError(null);
// Fetch operational page
console.log('Fetching operational pages...');
// console.log('Fetching operational pages...');
const pages = await operationalPageService.getOperationalPages();
console.log('All pages:', pages);
// console.log('All pages:', pages);
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
console.log('Found page:', foundPage);
// console.log('Found page:', foundPage);
if (!foundPage) {
console.log('Page not found or not public');
// console.log('Page not found or not public');
setError('Status page not found or not public');
setLoading(false);
return;
}
setPage(foundPage);
console.log('Page set successfully');
// console.log('Page set successfully');
// Fetch components for this page
console.log('Fetching components for page:', foundPage.id);
// console.log('Fetching components for page:', foundPage.id);
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
console.log('Components found:', pageComponents);
// console.log('Components found:', pageComponents);
setComponents(pageComponents);
// Fetch all services
console.log('Fetching all services...');
// console.log('Fetching all services...');
const allServices = await serviceService.getServices();
console.log('Services found:', allServices);
// console.log('Services found:', allServices);
setServices(allServices);
// Fetch uptime data for each component that has a service
console.log('Fetching uptime data...');
// console.log('Fetching uptime data...');
const uptimePromises = pageComponents
.filter(component => component.service_id)
.map(async (component) => {
try {
console.log('Fetching uptime for service:', component.service_id);
// console.log('Fetching uptime for service:', component.service_id);
const endDate = new Date();
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
// console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
return { serviceId: component.service_id, history };
} catch (error) {
console.error(`Error fetching uptime for service ${component.service_id}:`, error);
// console.error(`Error fetching uptime for service ${component.service_id}:`, error);
return { serviceId: component.service_id, history: [] };
}
});
@@ -84,12 +84,12 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
uptimeMap[result.serviceId] = result.history;
});
setUptimeData(uptimeMap);
console.log('Uptime data set successfully');
// console.log('Uptime data set successfully');
console.log('All data fetched successfully');
// console.log('All data fetched successfully');
} catch (err) {
console.error('Error fetching public page:', err);
// console.error('Error fetching public page:', err);
setError(`Failed to load status page: ${err}`);
} finally {
setLoading(false);
@@ -114,7 +114,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
textarea.setSelectionRange(0, 99999); // For mobile devices
}
} catch (selectError) {
console.error('Failed to select text:', selectError);
// console.error('Failed to select text:', selectError);
}
}
};
@@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentEndpoint } from "@/lib/pocketbase";
import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
import { OneClickInstallTab } from "./OneClickInstallTab";
import { DockerOneClickTab } from "./DockerOneClickTab";
import { ManualInstallTab } from "./ManualInstallTab";
interface AddServerAgentDialogProps {
@@ -123,9 +124,10 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
</TabsList>
@@ -150,6 +152,16 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="docker-one-click" className="space-y-6">
<DockerOneClickTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="manual" className="space-y-6">
<ManualInstallTab
@@ -0,0 +1,176 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download, Container } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface DockerOneClickTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getDockerOneClickCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
return `curl -L -o server-docker-agent.sh "${scriptUrl}"
chmod +x server-docker-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
sudo -E bash ./server-docker-agent.sh`;
};
const getDirectDockerCommand = () => {
return `docker run -d \\
--name monitoring-agent \\
--restart unless-stopped \\
-p 8081:8081 \\
--group-add 999 \\
-e AGENT_ID="${serverId}" \\
-e SERVER_NAME="${formData.serverName}" \\
-e SERVER_TOKEN="${serverToken}" \\
-e POCKETBASE_URL="${currentPocketBaseUrl}" \\
-e POCKETBASE_ENABLED=true \\
-v /proc:/host/proc:ro \\
-v /etc:/host/etc:ro \\
-v /sys:/host/sys:ro \\
-v /:/host/root:ro \\
-v /var/run:/host/var/run:ro \\
-v /dev:/host/dev:ro \\
-v /var/run/docker.sock:/var/run/docker.sock:ro \\
-v monitoring_data:/var/lib/monitoring-agent \\
-v monitoring_logs:/var/log/monitoring-agent \\
operacle/checkcle-server-agent:latest`;
};
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy one-click command button clicked');
const command = getDockerOneClickCommand();
// console.log('Copying one-click command:', command);
await copyToClipboard(command);
};
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy docker command button clicked');
const command = getDirectDockerCommand();
// console.log('Copying docker command:', command);
await copyToClipboard(command);
};
return (
<div className="space-y-6">
{/* One-Click Docker Installation */}
<Card className="border-blue-500/20 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" />
Docker One-Click Install
</CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300">
Automated Docker container installation with system monitoring capabilities
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
<div className="relative">
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
<code>{getDockerOneClickCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-blue-50 dark:bg-blue-950/50 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-400"
onClick={handleCopyOneClickCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
<p className="font-medium mb-1">This script will automatically:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Download and setup the Docker monitoring agent</li>
<li>Configure all required environment variables</li>
<li>Start the container with proper system access</li>
<li>Setup monitoring data persistence</li>
</ol>
</div>
</CardContent>
</Card>
{/* Direct Docker Run Command */}
<Card className="border-gray-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Container className="h-5 w-5" />
Direct Docker Run Command
</CardTitle>
<CardDescription>
If you prefer to run the Docker container directly without the script
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Docker Run Command</Label>
<div className="relative">
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{getDirectDockerCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={handleCopyDockerCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
<p className="font-medium mb-1">Prerequisites for direct Docker run:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Docker must be installed and running</li>
<li>The operacle/checkcle-server-agent image must be available</li>
<li>Run as root or with sudo privileges</li>
</ol>
</div>
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</div>
);
};
@@ -12,7 +12,7 @@ import { pb } from "@/lib/pocketbase";
import { Server } from "@/types/server.types";
import { RefreshCw, X } from "lucide-react";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { templateService, NotificationTemplate } from "@/services/templateService";
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
interface EditServerDialogProps {
@@ -25,7 +25,7 @@ interface EditServerDialogProps {
interface ServerFormData {
name: string;
check_interval: number;
retry_attempts: number;
max_retries: number;
docker_monitoring: boolean;
notification_enabled: boolean;
notification_channels: string[]; // Changed to array for multiple selections
@@ -49,7 +49,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const [formData, setFormData] = useState<ServerFormData>({
name: "",
check_interval: 60,
retry_attempts: 3,
max_retries: 3,
docker_monitoring: false,
notification_enabled: false,
notification_channels: [], // Changed to array
@@ -66,9 +66,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
const [templates, setTemplates] = useState<ServerNotificationTemplate[]>([]);
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<ServerNotificationTemplate | null>(null);
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
const [loadingTemplates, setLoadingTemplates] = useState(false);
@@ -78,7 +78,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Initialize form data when server changes
useEffect(() => {
if (server) {
// console.log("Setting form data for server:", server);
// Parse comma-separated notification_id into array
const notificationChannels = server.notification_id
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
@@ -87,9 +86,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
retry_attempts: 3,
max_retries: server.max_retries || 3,
docker_monitoring: server.docker === "true",
notification_enabled: notificationChannels.length > 0,
notification_enabled: server.notification_status === true,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
@@ -109,10 +108,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id
useEffect(() => {
if (server && server.threshold_id && thresholds.length > 0) {
// console.log("Loading existing threshold data for server:", server.threshold_id);
const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
if (existingThreshold) {
// console.log("Found existing threshold:", existingThreshold);
setSelectedThreshold(existingThreshold);
// Handle the API response format with proper field names and type conversion
setThresholdFormData({
@@ -166,7 +163,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs);
} catch (error) {
// console.error('Error loading alert configurations:', error);
toast({
variant: "destructive",
title: "Error",
@@ -180,10 +176,10 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const loadTemplates = async () => {
try {
setLoadingTemplates(true);
const templateList = await templateService.getTemplates();
setTemplates(templateList);
const templateList = await templateService.getTemplates('server');
// Cast to ServerNotificationTemplate[] since we know we're getting server templates
setTemplates(templateList as ServerNotificationTemplate[]);
} catch (error) {
// console.error('Error loading templates:', error);
toast({
variant: "destructive",
title: "Error",
@@ -200,7 +196,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const thresholdList = await serverThresholdService.getServerThresholds();
setThresholds(thresholdList);
} catch (error) {
// console.error('Error loading server thresholds:', error);
toast({
variant: "destructive",
title: "Error",
@@ -211,47 +206,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
}
};
const handleThresholdUpdate = async () => {
if (!selectedThreshold) return;
try {
// Use the correct field name for RAM threshold
const updateData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
toast({
title: "Threshold updated",
description: "Server threshold values have been updated successfully.",
});
} catch (error) {
// console.error('Error updating threshold:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to update threshold values.",
});
}
};
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
setFormData(prev => ({
...prev,
@@ -276,22 +230,56 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setIsSubmitting(true);
// Convert notification channels array to comma-separated string
const notificationChannelsString = formData.notification_enabled
? formData.notification_channels.join(',')
: "";
const notificationChannelsString = formData.notification_channels.join(',');
const updateData = {
name: formData.name,
check_interval: formData.check_interval,
max_retries: formData.max_retries,
docker: formData.docker_monitoring ? "true" : "false",
notification_status: formData.notification_enabled,
notification_id: notificationChannelsString,
threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "",
template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "",
threshold_id: formData.threshold_id !== "none" ? formData.threshold_id : "",
template_id: formData.template_id !== "none" ? formData.template_id : "",
updated: new Date().toISOString(),
};
await pb.collection('servers').update(server.id, updateData);
// Update threshold if a threshold is selected and values have been modified
if (selectedThreshold && formData.threshold_id !== "none") {
try {
const updateThresholdData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold,
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
} catch (error) {
toast({
variant: "destructive",
title: "Warning",
description: "Server updated but failed to update threshold values.",
});
}
}
toast({
title: "Server updated",
description: `${formData.name} has been updated successfully.`,
@@ -301,7 +289,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
onOpenChange(false);
} catch (error) {
// console.error('Error updating server:', error);
toast({
variant: "destructive",
title: "Error",
@@ -321,9 +308,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
retry_attempts: 3,
max_retries: server.max_retries || 3,
docker_monitoring: server.docker === "true",
notification_enabled: notificationChannels.length > 0,
notification_enabled: server.notification_status === true,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
@@ -372,20 +359,20 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
</div>
<div className="space-y-2">
<Label htmlFor="retryAttempts">Retry Attempts</Label>
<Label htmlFor="maxRetries">Max Retries</Label>
<Select
value={formData.retry_attempts.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, retry_attempts: parseInt(value) }))}
value={formData.max_retries.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue placeholder="Select retry attempts" />
<SelectValue placeholder="Select max retries" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 attempt</SelectItem>
<SelectItem value="2">2 attempts</SelectItem>
<SelectItem value="3">3 attempts</SelectItem>
<SelectItem value="5">5 attempts</SelectItem>
<SelectItem value="10">10 attempts</SelectItem>
<SelectItem value="1">1 retry</SelectItem>
<SelectItem value="2">2 retries</SelectItem>
<SelectItem value="3">3 retries</SelectItem>
<SelectItem value="5">5 retries</SelectItem>
<SelectItem value="10">10 retries</SelectItem>
</SelectContent>
</Select>
</div>
@@ -416,10 +403,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
checked={formData.notification_enabled}
onCheckedChange={(checked) => setFormData(prev => ({
...prev,
notification_enabled: checked,
notification_channels: checked ? prev.notification_channels : [],
threshold_id: checked ? prev.threshold_id : "none",
template_id: checked ? prev.template_id : "none"
notification_enabled: checked
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
}))}
/>
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
@@ -516,16 +501,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{/* Editable Threshold Details */}
{selectedThreshold && (
<Card className="bg-muted/50">
<CardHeader className="flex flex-row items-center justify-between">
<CardHeader>
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
<Button
type="button"
onClick={handleThresholdUpdate}
size="sm"
variant="outline"
>
Update Thresholds
</Button>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-3 text-sm">
@@ -621,27 +598,39 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 text-sm">
<div>
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold Message</Label>
<Label className="text-xs font-medium text-muted-foreground">RAM Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || "No RAM threshold message defined"}
{selectedTemplate.ram_message || "No RAM message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold Message</Label>
<Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || "No CPU threshold message defined"}
{selectedTemplate.cpu_message || "No CPU message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold Message</Label>
<Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.incident_message || "No disk threshold message defined"}
{selectedTemplate.disk_message || "No disk message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Network Threshold Message</Label>
<Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.maintenance_message || "No network threshold message defined"}
{selectedTemplate.network_message || "No network message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || "No up message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || "No down message defined"}
</p>
</div>
</div>
@@ -677,4 +666,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
</DialogContent>
</Dialog>
);
};
};
export default EditServerDialog;
@@ -25,7 +25,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
onDialogClose,
}) => {
const getManualInstallSteps = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return [
{
@@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
onDialogClose,
}) => {
const getOneClickInstallCommand = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return `curl -L -o server-agent.sh "${scriptUrl}"
chmod +x server-agent.sh
@@ -55,7 +55,7 @@ sudo -E bash ./server-agent.sh`;
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
<Download className="h-5 w-5" />
One-Click Install
One-Click Install (Recommended)
</CardTitle>
<CardDescription className="text-green-600 dark:text-green-300">
Copy and paste this single command to install the monitoring agent instantly
@@ -269,7 +269,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const isProcessing = pausingServers.has(server.id);
return (
<TableRow key={server.id} className="hover:bg-muted/50">
<TableRow
key={server.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleViewDetails(server.id)}
>
<TableCell className="font-medium">
<div className="truncate" title={server.name}>
{server.name}
@@ -325,7 +329,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
{new Date(server.last_checked).toLocaleString()}
</div>
</TableCell>
<TableCell className="text-right">
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
@@ -338,19 +342,19 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
<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={() => handleViewContainers(server.id)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
<Activity className="mr-2 h-4 w-4" />
Container Monitoring
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handlePauseResume(server)}
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
disabled={isProcessing}
>
{isPaused ? (
@@ -366,12 +370,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleEdit(server)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
<Edit className="mr-2 h-4 w-4" />
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(server)}
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -427,4 +431,4 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
</AlertDialog>
</>
);
};
};
@@ -8,6 +8,7 @@ import {
} from "@/components/ui/dialog";
import { ServiceForm } from "./ServiceForm";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useQueryClient } from "@tanstack/react-query";
interface AddServiceDialogProps {
open: boolean;
@@ -15,7 +16,11 @@ interface AddServiceDialogProps {
}
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
const handleSuccess = () => {
const queryClient = useQueryClient();
const handleSuccess = async () => {
// Immediately invalidate and refetch services data
await queryClient.invalidateQueries({ queryKey: ["services"] });
await queryClient.refetchQueries({ queryKey: ["services"] });
onOpenChange(false);
};
@@ -125,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
} else {
// Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|");
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
// console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
// console.log(`Retrieved ${history.length} regional monitoring records`);
}
@@ -5,7 +5,6 @@ import { Play, Pause } from "lucide-react";
import { Service } from "@/types/service.types";
import { serviceService } from "@/services/serviceService";
import { useToast } from "@/hooks/use-toast";
import { notificationService } from "@/services/notificationService";
interface ServiceMonitoringButtonProps {
service: Service;
@@ -34,16 +33,8 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
if (onStatusChange) onStatusChange("paused");
// Send notification for paused status (only here, not in pauseMonitoring.ts)
if (service.alerts !== "muted") {
// console.log("Sending pause notification from UI component");
// IMPORTANT: Direct call to the notification service to ensure a message is sent
await notificationService.sendNotification({
service: service,
status: "paused",
timestamp: new Date().toISOString(),
});
}
// Notification handling removed - will be handled by backend
// console.log("Service paused - notifications will be handled by backend");
toast({
title: "Monitoring paused",
@@ -51,7 +42,7 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
});
} else {
// Start/resume monitoring
// console.log(`Starting monitoring for service ${service.id} (${service.name})`);
// console.log(`Starting monitoring for service ${service.id} (${service.name})`);
// First ensure we update the status in the database to not be paused anymore
await serviceService.resumeMonitoring(service.id);
@@ -100,4 +91,4 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
)}
</Button>
);
}
}
@@ -13,6 +13,7 @@ import { serviceService } from "@/services/serviceService";
import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./ServiceRegionalFields";
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
import { useQueryClient } from "@tanstack/react-query";
interface ServiceFormProps {
onSuccess: () => void;
@@ -31,6 +32,7 @@ export function ServiceForm({
}: ServiceFormProps) {
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient();
// Initialize form with default values
const form = useForm<ServiceFormData>({
@@ -68,10 +70,10 @@ export function ServiceForm({
if (onSubmitStart) onSubmitStart();
try {
console.log("Form data being submitted:", data);
// console.log("Form data being submitted:", data);
const serviceData = mapFormDataToServiceData(data);
console.log("Service data being sent:", serviceData);
// console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -91,6 +93,16 @@ export function ServiceForm({
});
}
// Force immediate refresh of services data
queryClient.setQueryData(["services"], (oldData: any) => {
// Invalidate the cache to force a fresh fetch
return undefined;
});
// Invalidate and refetch services query
await queryClient.invalidateQueries({ queryKey: ["services"] });
await queryClient.refetchQueries({ queryKey: ["services"] });
onSuccess();
if (!isEdit) {
form.reset();
@@ -1,4 +1,3 @@
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
@@ -8,6 +7,7 @@ import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import { useQuery } from "@tanstack/react-query";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
import { useState, useEffect } from "react";
interface ServiceNotificationFieldsProps {
@@ -21,18 +21,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
const notificationStatus = form.watch("notificationStatus");
const notificationChannels = form.watch("notificationChannels") || [];
const alertTemplate = form.watch("alertTemplate");
// console.log("Current notification values:", {
// notificationStatus,
// notificationChannels,
// alertTemplate
// });
// Fetch alert configurations for notification channels
const { data: alertConfigsData } = useQuery({
queryKey: ['alertConfigs'],
queryFn: () => alertConfigService.getAlertConfigurations(),
});
// Fetch service notification templates
const { data: serviceTemplates, isLoading: isLoadingTemplates } = useQuery({
queryKey: ['serviceNotificationTemplates'],
queryFn: () => serviceNotificationTemplateService.getTemplates(),
});
// Update alert configs when data is loaded
useEffect(() => {
@@ -42,13 +42,19 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
setAlertConfigs(enabledChannels);
// Debug log to check what alert configs are loaded
// console.log("Loaded alert configurations:", enabledChannels);
}
}, [alertConfigsData]);
// Debug log for service templates
useEffect(() => {
if (serviceTemplates) {
// console.log("Loaded service notification templates:", serviceTemplates);
}
}, [serviceTemplates]);
// Log when form values change to debug
useEffect(() => {
// console.log("Notification values changed:", {
// console.log("Notification values changed:", {
// notificationStatus: form.getValues("notificationStatus"),
// notificationChannels: form.getValues("notificationChannels")
// });
@@ -159,9 +165,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
control={form.control}
name="alertTemplate"
render={({ field }) => {
// Don't convert existing values to "default"
const displayValue = field.value || "default";
// console.log("Rendering alert template field with value:", displayValue);
// console.log("Rendering alert template field with value:", field.value);
return (
<FormItem>
@@ -169,18 +173,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
<FormControl>
<Select
onValueChange={(value) => {
console.log("Alert template changed to:", value);
field.onChange(value === "default" ? "" : value);
field.onChange(value);
}}
value={displayValue}
disabled={notificationStatus !== "enabled"}
value={field.value || ""}
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder="Select an alert template" />
<SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
{/* Add templates here when available */}
{serviceTemplates?.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
@@ -195,4 +201,4 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
/>
</>
);
}
}
@@ -67,7 +67,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
placeholder={getPlaceholder()}
{...field}
onChange={(e) => {
console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
// console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
field.onChange(e);
}}
/>
@@ -93,7 +93,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
type="number"
{...field}
onChange={(e) => {
console.log("Port field changed:", e.target.value);
// console.log("Port field changed:", e.target.value);
field.onChange(e);
}}
/>
@@ -74,7 +74,6 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
notificationChannels.push(...parsedChannels);
}
} catch (error) {
// console.warn("Failed to parse notification_channel JSON:", error);
// If parsing fails, treat as single channel ID
notificationChannels.push(service.notification_channel);
}
@@ -91,16 +90,13 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
}
}
// console.log("Mapping service to form data:", {
// serviceName: service.name,
// notification_status: service.notification_status,
// notification_channel: service.notification_channel,
// notificationChannel: service.notificationChannel,
// mappedChannels: notificationChannels,
// regionalAgents: regionalAgents,
// region_name: service.region_name,
/// agent_id: service.agent_id
// });
// Handle notification_status - it can be boolean or string
let notificationStatus: "enabled" | "disabled" = "disabled";
if (typeof service.notification_status === "boolean") {
notificationStatus = service.notification_status ? "enabled" : "disabled";
} else if (typeof service.notification_status === "string") {
notificationStatus = service.notification_status === "enabled" ? "enabled" : "disabled";
}
return {
name: service.name || "",
@@ -109,7 +105,7 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
port: portValue,
interval: String(service.interval || 60),
retries: String(service.retries || 3),
notificationStatus: service.notification_status || "disabled",
notificationStatus: notificationStatus,
notificationChannels: notificationChannels,
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
regionalMonitoringEnabled: isRegionalEnabled,
@@ -152,7 +148,8 @@ export const mapFormDataToServiceData = (data: ServiceFormData) => {
type: data.type,
interval: parseInt(data.interval),
retries: parseInt(data.retries),
notificationStatus: data.notificationStatus || "disabled",
// Convert string status to boolean for notification_status field
notificationStatus: data.notificationStatus === "enabled",
notificationChannels: data.notificationChannels || [],
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
// Use regional_status field and store multiple agents as comma-separated values
@@ -95,7 +95,7 @@ export const AboutSystem: React.FC = () => {
<FileText className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
<span>{t('viewDocumentation')}</span>
</Button>
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("https://x.com/tlengoss", "_blank")}>
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("https://x.com/checkcle_oss", "_blank")}>
<Twitter className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
<span>{t('followOnX')}</span>
</Button>
@@ -1,9 +1,10 @@
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { templateService } from "@/services/templateService";
import { templateService, TemplateType } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, RefreshCcw } from "lucide-react";
import { TemplateList } from "./TemplateList";
import { TemplateDialog } from "./TemplateDialog";
@@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast";
export const AlertsTemplates = () => {
const { toast } = useToast();
const [activeTab, setActiveTab] = useState<TemplateType>('service');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
const [editingTemplateType, setEditingTemplateType] = useState<TemplateType | null>(null);
const {
data: templates = [],
@@ -20,17 +23,19 @@ export const AlertsTemplates = () => {
error,
refetch
} = useQuery({
queryKey: ['notification_templates'],
queryFn: templateService.getTemplates,
queryKey: ['notification_templates', activeTab],
queryFn: () => templateService.getTemplates(activeTab),
});
const handleAddTemplate = () => {
const handleAddTemplate = (templateType: TemplateType) => {
setEditingTemplate(null);
setEditingTemplateType(templateType);
setIsDialogOpen(true);
};
const handleEditTemplate = (id: string) => {
const handleEditTemplate = (id: string, templateType: TemplateType) => {
setEditingTemplate(id);
setEditingTemplateType(templateType);
setIsDialogOpen(true);
};
@@ -51,33 +56,103 @@ export const AlertsTemplates = () => {
<RefreshCcw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button onClick={handleAddTemplate}>
<Button onClick={() => handleAddTemplate(activeTab)}>
<Plus className="h-4 w-4 mr-2" />
Add Template
</Button>
</div>
</CardHeader>
<CardContent>
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={handleEditTemplate}
refetchTemplates={refetch}
/>
)}
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TemplateType)}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="service">Service Uptime</TabsTrigger>
<TabsTrigger value="server">Server Monitoring</TabsTrigger>
<TabsTrigger value="ssl">SSL Certificate</TabsTrigger>
<TabsTrigger value="server_threshold">Server Threshold</TabsTrigger>
</TabsList>
<TabsContent value="service" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading service templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'service')}
refetchTemplates={refetch}
templateType="service"
/>
)}
</TabsContent>
<TabsContent value="server" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading server templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'server')}
refetchTemplates={refetch}
templateType="server"
/>
)}
</TabsContent>
<TabsContent value="ssl" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading SSL templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'ssl')}
refetchTemplates={refetch}
templateType="ssl"
/>
)}
</TabsContent>
<TabsContent value="server_threshold" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading server threshold templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'server_threshold')}
refetchTemplates={refetch}
templateType="server_threshold"
/>
)}
</TabsContent>
</Tabs>
</CardContent>
<TemplateDialog
open={isDialogOpen}
templateId={editingTemplate}
templateType={editingTemplateType}
onOpenChange={setIsDialogOpen}
onSuccess={() => {
refetch();
@@ -86,4 +161,4 @@ export const AlertsTemplates = () => {
/>
</Card>
);
};
};
@@ -1,19 +1,26 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useTemplateForm } from "./hooks/useTemplateForm";
import { BasicTemplateFields } from "./form/BasicTemplateFields";
import { MessagesTabContent } from "./form/MessagesTabContent";
import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent";
import { ServerTemplateFields } from "./form/ServerTemplateFields";
import { ServiceTemplateFields } from "./form/ServiceTemplateFields";
import { SslTemplateFields } from "./form/SslTemplateFields";
import { ServerThresholdFields } from "./form/ServerThresholdFields";
import { Loader2, ChevronDown } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
interface TemplateDialogProps {
open: boolean;
templateId: string | null;
templateType: TemplateType | null;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
@@ -21,9 +28,12 @@ interface TemplateDialogProps {
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
open,
templateId,
templateType: initialTemplateType,
onOpenChange,
onSuccess,
}) => {
const [selectedTemplateType, setSelectedTemplateType] = useState<TemplateType>(initialTemplateType || 'service');
const {
form,
isEditMode,
@@ -32,28 +42,72 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
onSubmit
} = useTemplateForm({
templateId,
templateType: selectedTemplateType,
open,
onOpenChange,
onSuccess
});
// For debugging purposes
// Update template type when prop changes or dialog opens
useEffect(() => {
if (open) {
// console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId);
// Log form values when they change
const subscription = form.watch((value) => {
// console.log("Current form values:", value);
});
return () => subscription.unsubscribe();
if (initialTemplateType) {
setSelectedTemplateType(initialTemplateType);
} else if (open && !isEditMode) {
setSelectedTemplateType('service');
}
}, [open, isEditMode, templateId, form]);
}, [initialTemplateType, open, isEditMode]);
// Handle template type change
const handleTemplateTypeChange = (newType: TemplateType) => {
if (!isEditMode) {
setSelectedTemplateType(newType);
form.setValue('templateType', newType);
}
};
const renderTemplateFields = () => {
switch (selectedTemplateType) {
case 'server':
return <ServerTemplateFields control={form.control} />;
case 'service':
return <ServiceTemplateFields control={form.control} />;
case 'ssl':
return <SslTemplateFields control={form.control} />;
case 'server_threshold':
return <ServerThresholdFields control={form.control} />;
default:
return null;
}
};
const renderPlaceholderGuide = () => {
const config = templateTypeConfigs[selectedTemplateType];
if (!config) return null;
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Available Placeholders</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
{config.description}. Use these placeholders in your messages:
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
{config.placeholders.map((placeholder) => (
<div key={placeholder} className="bg-muted/30 p-2 rounded">
<code className="text-xs">{placeholder}</code>
</div>
))}
</div>
</CardContent>
</Card>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
</DialogHeader>
@@ -69,20 +123,85 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="relative flex-1">
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
<div className="space-y-6 pb-6 pr-4">
<BasicTemplateFields control={form.control} />
{/* Basic Fields */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Template Name</FormLabel>
<FormControl>
<Input
placeholder="Enter template name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="templateType"
render={({ field }) => (
<FormItem>
<FormLabel>Template Type</FormLabel>
<FormControl>
<Select
onValueChange={handleTemplateTypeChange}
value={selectedTemplateType}
disabled={isEditMode}
>
<SelectTrigger>
<SelectValue placeholder="Select template type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="server">Server Monitoring</SelectItem>
<SelectItem value="service">Service Uptime</SelectItem>
<SelectItem value="ssl">SSL Certificate</SelectItem>
<SelectItem value="server_threshold">Server Threshold</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{selectedTemplateType !== 'server_threshold' && (
<FormField
control={form.control}
name="placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Placeholder</FormLabel>
<FormControl>
<Input
placeholder="Optional custom placeholder"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<Tabs defaultValue="messages">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="messages">Messages</TabsTrigger>
<TabsTrigger value="messages">{selectedTemplateType === 'server_threshold' ? 'Thresholds' : 'Messages'}</TabsTrigger>
<TabsTrigger value="placeholders">Placeholders</TabsTrigger>
</TabsList>
<TabsContent value="messages" className="pt-4">
<MessagesTabContent control={form.control} />
{renderTemplateFields()}
</TabsContent>
<TabsContent value="placeholders" className="pt-4">
<PlaceholdersTabContent control={form.control} />
{renderPlaceholderGuide()}
</TabsContent>
</Tabs>
</div>
@@ -120,4 +239,4 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</DialogContent>
</Dialog>
);
};
};
@@ -1,17 +1,15 @@
import React, { useState } from "react";
import { NotificationTemplate, templateService } from "@/services/templateService";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Edit, Trash2 } from "lucide-react";
import {
import { Badge } from "@/components/ui/badge";
import { Trash2, Edit, MoreVertical } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
@@ -22,63 +20,75 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast";
import { Badge } from "@/components/ui/badge";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, AnyTemplate, TemplateType } from "@/services/templateService";
interface TemplateListProps {
templates: NotificationTemplate[];
templates: AnyTemplate[];
isLoading: boolean;
onEdit: (id: string) => void;
refetchTemplates: () => void;
templateType: TemplateType;
}
export const TemplateList: React.FC<TemplateListProps> = ({
templates,
isLoading,
onEdit,
export const TemplateList: React.FC<TemplateListProps> = ({
templates,
isLoading,
onEdit,
refetchTemplates,
templateType
}) => {
const { toast } = useToast();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<NotificationTemplate | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const queryClient = useQueryClient();
const [deleteTemplateId, setDeleteTemplateId] = useState<string | null>(null);
const handleDeletePrompt = (template: NotificationTemplate) => {
setTemplateToDelete(template);
setDeleteDialogOpen(true);
};
const handleDeleteTemplate = async () => {
if (!templateToDelete) return;
setIsDeleting(true);
try {
await templateService.deleteTemplate(templateToDelete.id);
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (id: string) => templateService.deleteTemplate(id, templateType),
onSuccess: () => {
toast({
title: "Template deleted",
description: `Template "${templateToDelete.name}" has been removed.`,
description: "The template has been deleted successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
refetchTemplates();
} catch (error) {
console.error("Error deleting template:", error);
},
onError: (error) => {
// console.error("Error deleting template:", error);
toast({
title: "Error",
description: "Failed to delete template. Please try again.",
variant: "destructive",
});
} finally {
setIsDeleting(false);
setDeleteDialogOpen(false);
setTemplateToDelete(null);
},
});
const handleDelete = (id: string) => {
setDeleteTemplateId(id);
};
const confirmDelete = () => {
if (deleteTemplateId) {
deleteMutation.mutate(deleteTemplateId);
setDeleteTemplateId(null);
}
};
if (isLoading) {
return (
<div className="py-8 flex justify-center">
<div className="animate-pulse flex flex-col items-center">
<div className="h-4 bg-gray-200 rounded w-32 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-64"></div>
</div>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border border-border rounded-lg">
<div className="space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-32"></div>
<div className="h-3 bg-muted animate-pulse rounded w-48"></div>
</div>
<div className="flex space-x-2">
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
</div>
</div>
))}
</div>
);
}
@@ -86,83 +96,86 @@ export const TemplateList: React.FC<TemplateListProps> = ({
if (templates.length === 0) {
return (
<div className="text-center py-8">
<p className="text-muted-foreground mb-2">No templates found</p>
<p className="text-sm text-muted-foreground">
Create your first notification template to get started.
</p>
<p className="text-muted-foreground">No templates found. Create your first template to get started.</p>
</div>
);
}
const getTemplateTypeLabel = (type: TemplateType) => {
switch (type) {
case 'server': return 'Server';
case 'service': return 'Service';
case 'ssl': return 'SSL';
default: return 'Unknown';
}
};
return (
<>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="font-medium">{template.name}</TableCell>
<TableCell>
<Badge variant="outline">{template.type}</Badge>
</TableCell>
<TableCell>
{new Date(template.created).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(template.id)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeletePrompt(template)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="space-y-4">
{templates.map((template) => (
<div key={template.id} className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">{template.name}</h3>
<Badge variant="outline">{getTemplateTypeLabel(templateType)}</Badge>
</div>
<p className="text-sm text-muted-foreground">
Created: {new Date(template.created).toLocaleDateString()}
{template.updated !== template.created &&
` • Updated: ${new Date(template.updated).toLocaleDateString()}`
}
</p>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(template.id)}
>
<Edit className="h-4 w-4 mr-1" />
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleDelete(template.id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialog open={!!deleteTemplateId} onOpenChange={() => setDeleteTemplateId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Template</AlertDialogTitle>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone.
This action cannot be undone. This will permanently delete the template.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDeleteTemplate();
}}
disabled={isDeleting}
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? "Deleting..." : "Delete"}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
};
@@ -71,15 +71,168 @@ export const PlaceholdersTabContent: React.FC<PlaceholdersTabContentProps> = ({
<FormField
control={control}
name="threshold_placeholder"
name="url_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Threshold Placeholder</FormLabel>
<FormLabel>URL Placeholder</FormLabel>
<FormControl>
<Input placeholder="${threshold}" {...field} />
<Input placeholder="${url}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for threshold values in alerts
Used for service URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="host_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Host/IP Placeholder</FormLabel>
<FormControl>
<Input placeholder="${host}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service host or IP address
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="service_type_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Service Type Placeholder</FormLabel>
<FormControl>
<Input placeholder="${service_type}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service type (HTTP, PING, TCP, DNS)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="port_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Port Placeholder</FormLabel>
<FormControl>
<Input placeholder="${port}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service port number
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="domain_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Domain Placeholder</FormLabel>
<FormControl>
<Input placeholder="${domain}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for domain name (DNS services)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="region_name_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Region Name Placeholder</FormLabel>
<FormControl>
<Input placeholder="${region_name}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for regional agent name
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="agent_id_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Agent ID Placeholder</FormLabel>
<FormControl>
<Input placeholder="${agent_id}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for regional agent ID
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="uptime_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Uptime Placeholder</FormLabel>
<FormControl>
<Input placeholder="${uptime}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service uptime percentage
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="time_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Time Placeholder</FormLabel>
<FormControl>
<Input placeholder="${time}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for current date and time
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="error_message_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Error Message Placeholder</FormLabel>
<FormControl>
<Input placeholder="${error_message}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for error details when service is down
</FormDescription>
<FormMessage />
</FormItem>
@@ -111,22 +264,50 @@ export const PlaceholdersTabContent: React.FC<PlaceholdersTabContentProps> = ({
<code className="text-xs">${"{status}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service status (UP, DOWN)</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{threshold}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service threshold value</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{url}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service URL</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{host}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service host or IP address</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{service_type}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service type (HTTP, PING, TCP, DNS)</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{port}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service port number</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{domain}"}</code>
<p className="text-xs text-muted-foreground mt-1">Domain name (DNS services)</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{region_name}"}</code>
<p className="text-xs text-muted-foreground mt-1">Regional agent name</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{agent_id}"}</code>
<p className="text-xs text-muted-foreground mt-1">Regional agent ID</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{uptime}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service uptime percentage</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{time}"}</code>
<p className="text-xs text-muted-foreground mt-1">Current date and time</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{error_message}"}</code>
<p className="text-xs text-muted-foreground mt-1">Error details when service is down</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
};
@@ -0,0 +1,328 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServerTemplateFieldsProps {
control: Control<any>;
}
export const ServerTemplateFields: React.FC<ServerTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">System Resource Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="cpu_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="ram_message"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="network_message"
render={({ field }) => (
<FormItem>
<FormLabel>Network Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">System Resource Restore Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="restore_cpu_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU usage on ${server_name} has returned to normal: ${cpu_usage}%"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="restore_ram_message"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="Memory usage on ${server_name} has returned to normal: ${ram_usage}%"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="restore_disk_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk usage on ${server_name} has returned to normal: ${disk_usage}%"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="restore_network_message"
render={({ field }) => (
<FormItem>
<FormLabel>Network Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="Network usage on ${server_name} has returned to normal: ${network_usage}%"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is UP and responding"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is DOWN"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Server ${server_name} requires attention"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="paused_message"
render={({ field }) => (
<FormItem>
<FormLabel>Paused Message</FormLabel>
<FormControl>
<Textarea
placeholder="Monitoring for server ${server_name} is paused"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="cpu_temp_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Temperature Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU temperature on ${server_name} is ${cpu_temp}°C"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_io_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk I/O Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk I/O on ${server_name} is ${disk_io} MB/s"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="restore_cpu_temp_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Temperature Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU temperature on ${server_name} has returned to normal: ${cpu_temp}°C"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="restore_disk_io_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk I/O Restore Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk I/O on ${server_name} has returned to normal: ${disk_io} MB/s"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,121 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServerThresholdFieldsProps {
control: Control<any>;
}
export const ServerThresholdFields: React.FC<ServerThresholdFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Server Resource Thresholds</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="cpu_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="85"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
CPU usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="ram_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="80"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Memory usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="90"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Disk usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="network_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Network Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="75"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Network usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,133 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServiceTemplateFieldsProps {
control: Control<any>;
}
export const ServiceTemplateFields: React.FC<ServiceTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Service Status Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is UP. Response time: ${response_time}ms"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is DOWN. Status: ${status}"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="maintenance_message"
render={({ field }) => (
<FormItem>
<FormLabel>Maintenance Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is under maintenance"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="incident_message"
render={({ field }) => (
<FormItem>
<FormLabel>Incident Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} has an incident"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="resolved_message"
render={({ field }) => (
<FormItem>
<FormLabel>Resolved Message</FormLabel>
<FormControl>
<Textarea
placeholder="Issue with service ${service_name} has been resolved"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Service ${service_name} response time is high"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,79 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface SslTemplateFieldsProps {
control: Control<any>;
}
export const SslTemplateFields: React.FC<SslTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">SSL Certificate Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<FormField
control={control}
name="expired"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expired Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} has EXPIRED on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="exiring_soon"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expiring Soon Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: SSL certificate for ${domain} requires attention"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -1,2 +1,2 @@
export * from './useTemplateForm';
export * from './useTemplateForm';
@@ -1,72 +1,180 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useToast } from "@/hooks/use-toast";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, CreateUpdateTemplateData } from "@/services/templateService";
import { templateService, TemplateType, AnyTemplateData } from "@/services/templateService";
import { useEffect } from "react";
// Template form schema
export const templateFormSchema = z.object({
// Base schema
const baseSchema = {
name: z.string().min(2, "Name is required and must be at least 2 characters"),
type: z.string().min(1, "Type is required"),
templateType: z.enum(['server', 'service', 'ssl', 'server_threshold'] as const),
placeholder: z.string().optional(),
};
// Server template schema
const serverTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('server'),
ram_message: z.string().min(1, "RAM message is required"),
cpu_message: z.string().min(1, "CPU message is required"),
disk_message: z.string().min(1, "Disk message is required"),
network_message: z.string().min(1, "Network message is required"),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
notification_id: z.string().optional(),
warning_message: z.string().min(1, "Warning message is required"),
paused_message: z.string().min(1, "Paused message is required"),
cpu_temp_message: z.string().min(1, "CPU temperature message is required"),
disk_io_message: z.string().min(1, "Disk I/O message is required"),
restore_ram_message: z.string().optional(),
restore_cpu_message: z.string().optional(),
restore_disk_message: z.string().optional(),
restore_network_message: z.string().optional(),
restore_cpu_temp_message: z.string().optional(),
restore_disk_io_message: z.string().optional(),
});
// Service template schema
const serviceTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('service'),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
maintenance_message: z.string().min(1, "Maintenance message is required"),
incident_message: z.string().min(1, "Incident message is required"),
resolved_message: z.string().min(1, "Resolved message is required"),
service_name_placeholder: z.string().min(1, "Service name placeholder is required"),
response_time_placeholder: z.string().min(1, "Response time placeholder is required"),
status_placeholder: z.string().min(1, "Status placeholder is required"),
threshold_placeholder: z.string().min(1, "Threshold placeholder is required"),
warning_message: z.string().min(1, "Warning message is required"),
});
// Define the form data type from the schema
// SSL template schema
const sslTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('ssl'),
expired: z.string().min(1, "Expired message is required"),
exiring_soon: z.string().min(1, "Expiring soon message is required"),
warning: z.string().min(1, "Warning message is required"),
});
// Server threshold schema
const serverThresholdSchema = z.object({
...baseSchema,
templateType: z.literal('server_threshold'),
cpu_threshold: z.number().min(0).max(100, "CPU threshold must be between 0-100"),
ram_threshold: z.number().min(0).max(100, "RAM threshold must be between 0-100"),
disk_threshold: z.number().min(0).max(100, "Disk threshold must be between 0-100"),
network_threshold: z.number().min(0).max(100, "Network threshold must be between 0-100"),
notification_id: z.string().optional(),
server_template_id: z.string().optional(),
});
// Combined schema
export const templateFormSchema = z.discriminatedUnion("templateType", [
serverTemplateSchema,
serviceTemplateSchema,
sslTemplateSchema,
serverThresholdSchema,
]);
export type TemplateFormData = z.infer<typeof templateFormSchema>;
// Default form values
const defaultFormValues: TemplateFormData = {
name: "",
type: "default",
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Service ${service_name} issue has been resolved",
service_name_placeholder: "${service_name}",
response_time_placeholder: "${response_time}",
status_placeholder: "${status}",
threshold_placeholder: "${threshold}",
// Default form values for each template type
const getDefaultValues = (templateType: TemplateType): TemplateFormData => {
const base = {
name: "",
templateType,
placeholder: "",
};
switch (templateType) {
case 'server':
return {
...base,
templateType: 'server' as const,
ram_message: "Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)",
cpu_message: "CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)",
disk_message: "Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)",
network_message: "Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)",
up_message: "Server ${server_name} is UP and responding",
down_message: "Server ${server_name} is DOWN",
notification_id: "",
warning_message: "Warning: Server ${server_name} requires attention",
paused_message: "Monitoring for server ${server_name} is paused",
cpu_temp_message: "CPU temperature on ${server_name} is ${cpu_temp}°C",
disk_io_message: "Disk I/O on ${server_name} is ${disk_io} MB/s",
restore_ram_message: "Memory usage on ${server_name} has returned to normal: ${ram_usage}%",
restore_cpu_message: "CPU usage on ${server_name} has returned to normal: ${cpu_usage}%",
restore_disk_message: "Disk usage on ${server_name} has returned to normal: ${disk_usage}%",
restore_network_message: "Network usage on ${server_name} has returned to normal: ${network_usage}%",
restore_cpu_temp_message: "CPU temperature on ${server_name} has returned to normal: ${cpu_temp}°C",
restore_disk_io_message: "Disk I/O on ${server_name} has returned to normal: ${disk_io} MB/s",
};
case 'service':
return {
...base,
templateType: 'service' as const,
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Issue with service ${service_name} has been resolved",
warning_message: "Warning: Service ${service_name} response time is high",
};
case 'ssl':
return {
...base,
templateType: 'ssl' as const,
expired: "SSL certificate for ${domain} has EXPIRED on ${expiry_date}",
exiring_soon: "SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}",
warning: "Warning: SSL certificate for ${domain} requires attention",
};
case 'server_threshold':
return {
...base,
templateType: 'server_threshold' as const,
cpu_threshold: 85,
ram_threshold: 80,
disk_threshold: 90,
network_threshold: 75,
notification_id: "",
server_template_id: "",
};
default:
throw new Error(`Unknown template type: ${templateType}`);
}
};
export interface UseTemplateFormProps {
templateId: string | null;
templateType: TemplateType;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
export const useTemplateForm = ({ templateId, templateType, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
const { toast } = useToast();
const queryClient = useQueryClient();
const isEditMode = !!templateId;
// console.log("Template form initialized with templateId:", templateId, "isEditMode:", isEditMode);
const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema),
defaultValues: defaultFormValues,
defaultValues: getDefaultValues(templateType),
mode: "onChange"
});
// Query to fetch template data for editing
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
queryKey: ['template', templateId],
queryKey: ['template', templateId, templateType],
queryFn: () => {
if (!templateId) return null;
console.log("Fetching template data for ID:", templateId);
return templateService.getTemplate(templateId);
return templateService.getTemplate(templateId, templateType);
},
enabled: !!templateId && open,
});
@@ -74,53 +182,71 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
// Set form values when template data is loaded
useEffect(() => {
if (templateData && open) {
// console.log("Setting form values with template data:", templateData);
form.reset({
const formData: any = {
name: templateData.name || "",
type: templateData.type || "default",
up_message: templateData.up_message || "",
down_message: templateData.down_message || "",
maintenance_message: templateData.maintenance_message || "",
incident_message: templateData.incident_message || "",
resolved_message: templateData.resolved_message || "",
service_name_placeholder: templateData.service_name_placeholder || "${service_name}",
response_time_placeholder: templateData.response_time_placeholder || "${response_time}",
status_placeholder: templateData.status_placeholder || "${status}",
threshold_placeholder: templateData.threshold_placeholder || "${threshold}",
});
}
}, [templateData, open, form]);
templateType: templateType,
placeholder: (templateData as any).placeholder || "",
};
// Handle form errors
useEffect(() => {
const subscription = form.formState.errors.root?.message &&
toast({
title: "Error",
description: form.formState.errors.root.message,
variant: "destructive",
// Define the expected fields for each template type
const expectedFields = {
server: [
'ram_message', 'cpu_message', 'disk_message', 'network_message',
'up_message', 'down_message', 'notification_id', 'warning_message',
'paused_message', 'cpu_temp_message', 'disk_io_message',
'restore_ram_message', 'restore_cpu_message', 'restore_disk_message',
'restore_network_message', 'restore_cpu_temp_message', 'restore_disk_io_message'
],
service: [
'up_message', 'down_message', 'maintenance_message', 'incident_message',
'resolved_message', 'warning_message'
],
ssl: ['expired', 'exiring_soon', 'warning'],
server_threshold: [
'cpu_threshold', 'ram_threshold', 'disk_threshold', 'network_threshold',
'notification_id', 'server_template_id'
]
};
// Add template-specific fields
const fieldsToProcess = expectedFields[templateType] || [];
fieldsToProcess.forEach(key => {
let value = (templateData as any)[key];
// Convert string values to numbers for server threshold fields
if (templateType === 'server_threshold' &&
['cpu_threshold', 'ram_threshold', 'disk_threshold', 'network_threshold'].includes(key)) {
value = typeof value === 'string' ? Number(value) : value;
// Ensure valid number, fallback to default if invalid
if (isNaN(value)) {
const defaults = getDefaultValues(templateType) as any;
value = defaults[key] || 0;
}
}
// Set the value, using empty string as fallback for string fields
formData[key] = value !== undefined && value !== null ? value : "";
});
return () => {
if (subscription) {
// Clean up if needed
}
};
}, [form.formState.errors.root, toast]);
console.log("Final form data being set:", formData);
form.reset(formData);
}
}, [templateData, open, form, templateType]);
// Create mutation
const createMutation = useMutation({
mutationFn: (data: CreateUpdateTemplateData) => templateService.createTemplate(data),
mutationFn: (data: AnyTemplateData) => templateService.createTemplate(data, templateType),
onSuccess: () => {
toast({
title: "Template created",
description: "Your notification template has been created successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess();
},
onError: (error) => {
// console.error("Error creating template:", error);
toast({
title: "Error",
description: "Failed to create template. Please check your inputs and try again.",
@@ -131,18 +257,18 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
// Update mutation
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateUpdateTemplateData }) =>
templateService.updateTemplate(id, data),
mutationFn: ({ id, data }: { id: string; data: AnyTemplateData }) =>
templateService.updateTemplate(id, data, templateType),
onSuccess: () => {
toast({
title: "Template updated",
description: "Your notification template has been updated successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess();
},
onError: (error) => {
// console.error("Error updating template:", error);
toast({
title: "Error",
description: "Failed to update template. Please check your inputs and try again.",
@@ -155,39 +281,28 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
// Handle form submission
const onSubmit = (formData: TemplateFormData) => {
// console.log("Submitting form data:", formData);
// Ensure all required fields are present
const completeData: CreateUpdateTemplateData = {
name: formData.name,
type: formData.type,
up_message: formData.up_message,
down_message: formData.down_message,
maintenance_message: formData.maintenance_message,
incident_message: formData.incident_message,
resolved_message: formData.resolved_message,
service_name_placeholder: formData.service_name_placeholder,
response_time_placeholder: formData.response_time_placeholder,
status_placeholder: formData.status_placeholder,
threshold_placeholder: formData.threshold_placeholder,
};
// Remove templateType from the data before sending to API
const { templateType: _, ...templateDataWithoutType } = formData;
const completeData = templateDataWithoutType as AnyTemplateData;
if (isEditMode && templateId) {
// console.log("Updating template with ID:", templateId);
updateMutation.mutate({ id: templateId, data: completeData });
} else {
// console.log("Creating new template");
createMutation.mutate(completeData);
}
};
// Reset form when dialog closes
// Reset form when dialog closes or template type changes
useEffect(() => {
if (!open) {
// console.log("Dialog closed, resetting form");
form.reset(defaultFormValues);
console.log("Dialog closed, resetting form");
form.reset(getDefaultValues(templateType));
}
}, [open, form]);
}, [open, form, templateType]);
return {
form,
@@ -196,4 +311,4 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
isSubmitting,
onSubmit
};
};
};
@@ -1,4 +1,3 @@
import React, { useEffect } from "react";
import {
Dialog,
@@ -8,12 +7,13 @@ import {
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Form,
FormControl,
@@ -24,7 +24,9 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Loader2 } from "lucide-react";
import { Loader2, Copy } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "@/hooks/use-toast";
interface NotificationChannelDialogProps {
open: boolean;
@@ -34,9 +36,9 @@ interface NotificationChannelDialogProps {
const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
enabled: z.boolean().default(true),
service_id: z.string().default("global"), // Assuming global for now, could be linked to specific services
service_id: z.string().default("global"),
template_id: z.string().optional(),
});
@@ -59,11 +61,27 @@ const slackSchema = baseSchema.extend({
const signalSchema = baseSchema.extend({
notification_type: z.literal("signal"),
signal_number: z.string().min(1, "Signal number is required"),
signal_api_endpoint: z.string().url("Must be a valid API endpoint URL"),
});
const googleChatSchema = baseSchema.extend({
notification_type: z.literal("google_chat"),
google_chat_webhook_url: z.string().url("Must be a valid URL"),
});
const emailSchema = baseSchema.extend({
notification_type: z.literal("email"),
// Email specific fields could be added here
email_address: z.string().email("Valid email is required"),
email_sender_name: z.string().min(1, "Sender name is required"),
smtp_server: z.string().min(1, "SMTP server is required"),
smtp_port: z.string().min(1, "SMTP port is required"),
smtp_password: z.string().min(1, "SMTP password is required"),
});
const webhookSchema = baseSchema.extend({
notification_type: z.literal("webhook"),
webhook_url: z.string().url("Must be a valid URL"),
webhook_payload_template: z.string().optional(),
});
const formSchema = z.discriminatedUnion("notification_type", [
@@ -71,11 +89,104 @@ const formSchema = z.discriminatedUnion("notification_type", [
discordSchema,
slackSchema,
signalSchema,
emailSchema
googleChatSchema,
emailSchema,
webhookSchema
]);
type FormValues = z.infer<typeof formSchema>;
const notificationTypeOptions = [
{
value: "telegram",
label: "Telegram",
description: "Send notifications via Telegram bot",
icon: "/upload/notification/telegram.png"
},
{
value: "discord",
label: "Discord",
description: "Send notifications to Discord webhook",
icon: "/upload/notification/discord.png"
},
{
value: "slack",
label: "Slack",
description: "Send notifications to Slack webhook",
icon: "/upload/notification/slack.png"
},
{
value: "signal",
label: "Signal",
description: "Send notifications via Signal",
icon: "/upload/notification/signal.png"
},
{
value: "google_chat",
label: "Google Chat",
description: "Send notifications to Google Chat webhook",
icon: "/upload/notification/google.png"
},
{
value: "email",
label: "Email",
description: "Send notifications via email",
icon: "/upload/notification/email.png"
},
{
value: "webhook",
label: "Webhook",
description: "Send notifications to custom webhook",
icon: "/upload/notification/webhook.png"
},
];
const webhookPayloadTemplates = {
server: `{
"alert_type": "server",
"server_name": "\${server_name}",
"status": "\${status}",
"cpu_usage": "\${cpu_usage}",
"ram_usage": "\${ram_usage}",
"disk_usage": "\${disk_usage}",
"network_usage": "\${network_usage}",
"cpu_temp": "\${cpu_temp}",
"disk_io": "\${disk_io}",
"threshold": "\${time}",
"message": "Server \${server_name} alert: \${status}"
}`,
service: `{
"alert_type": "service",
"service_name": "\${service_name}",
"status": "\${status}",
"response_time": "\${response_time}",
"url": "\${url}",
"uptime": "\${uptime}",
"downtime": "\${downtime}",
"timestamp": "\${time}",
"message": "Service \${service_name} is \${status}"
}`,
ssl: `{
"alert_type": "ssl",
"domain": "\${domain}",
"certificate_name": "\${certificate_name}",
"expiry_date": "\${expiry_date}",
"days_left": "\${days_left}",
"issuer": "\${issuer}",
"serial_number": "\${serial_number}",
"timestamp": "\${time}",
"message": "SSL certificate for \${domain} expires in \${days_left} days"
}`
};
const defaultPayloadTemplate = `{
"alert_type": "general",
"service_name": "\${service_name}",
"status": "\${status}",
"message": "\${message}",
"timestamp": "\${time}"
}`;
export const NotificationChannelDialog = ({
open,
onClose,
@@ -93,7 +204,7 @@ export const NotificationChannelDialog = ({
},
});
const { watch, reset } = form;
const { watch, reset, setValue } = form;
const notificationType = watch("notification_type");
const [isSubmitting, setIsSubmitting] = React.useState(false);
@@ -123,10 +234,14 @@ export const NotificationChannelDialog = ({
onClose(false);
};
const insertTemplate = (template: string) => {
setValue("webhook_payload_template", template);
};
const onSubmit = async (values: FormValues) => {
setIsSubmitting(true);
try {
// Ensure service_id is always present
// Handle all notification types including webhook through alert_configurations
const configData = {
...values,
service_id: values.service_id || "global",
@@ -137,7 +252,14 @@ export const NotificationChannelDialog = ({
} else {
await alertConfigService.createAlertConfiguration(configData as any);
}
onClose(true); // Close with refresh
} catch (error) {
toast({
title: "Error",
description: "Failed to save notification channel",
variant: "destructive"
});
} finally {
setIsSubmitting(false);
}
@@ -145,7 +267,7 @@ export const NotificationChannelDialog = ({
return (
<Dialog open={open} onOpenChange={() => handleClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{isEditing ? "Edit Notification Channel" : "Add Notification Channel"}
@@ -159,7 +281,7 @@ export const NotificationChannelDialog = ({
name="notify_name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>Channel Name</FormLabel>
<FormControl>
<Input placeholder="My Notification Channel" {...field} />
</FormControl>
@@ -175,63 +297,37 @@ export const NotificationChannelDialog = ({
control={form.control}
name="notification_type"
render={({ field }) => (
<FormItem className="space-y-3">
<FormItem>
<FormLabel>Channel Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="telegram" />
</FormControl>
<FormLabel className="font-normal">
Telegram
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="discord" />
</FormControl>
<FormLabel className="font-normal">
Discord
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="slack" />
</FormControl>
<FormLabel className="font-normal">
Slack
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="signal" />
</FormControl>
<FormLabel className="font-normal">
Signal
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="email" />
</FormControl>
<FormLabel className="font-normal">
Email
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select notification type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{notificationTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center space-x-3">
<img
src={option.icon}
alt={`${option.label} icon`}
className="w-5 h-5 object-contain"
/>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Show different fields based on notification type */}
{notificationType === "telegram" && (
<>
<FormField
@@ -243,6 +339,9 @@ export const NotificationChannelDialog = ({
<FormControl>
<Input placeholder="Telegram Chat ID" {...field} />
</FormControl>
<FormDescription>
The Telegram chat ID to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -256,6 +355,9 @@ export const NotificationChannelDialog = ({
<FormControl>
<Input placeholder="Telegram Bot Token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Telegram bot token from @BotFather
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -271,8 +373,11 @@ export const NotificationChannelDialog = ({
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Discord Webhook URL" {...field} />
<Input placeholder="https://discord.com/api/webhooks/..." {...field} />
</FormControl>
<FormDescription>
Discord webhook URL from your server settings
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -287,8 +392,11 @@ export const NotificationChannelDialog = ({
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Slack Webhook URL" {...field} />
<Input placeholder="https://hooks.slack.com/services/..." {...field} />
</FormControl>
<FormDescription>
Slack incoming webhook URL
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -296,20 +404,230 @@ export const NotificationChannelDialog = ({
)}
{notificationType === "signal" && (
<>
<FormField
control={form.control}
name="signal_number"
render={({ field }) => (
<FormItem>
<FormLabel>Signal Number</FormLabel>
<FormControl>
<Input placeholder="+1234567890" {...field} />
</FormControl>
<FormDescription>
Signal phone number to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="signal_api_endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Signal API Endpoint</FormLabel>
<FormControl>
<Input placeholder="https://your-signal-api.com/v2/send" {...field} />
</FormControl>
<FormDescription>
The Rest API endpoint for your Signal service
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "google_chat" && (
<FormField
control={form.control}
name="signal_number"
name="google_chat_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Signal Number</FormLabel>
<FormLabel>Google Chat Webhook URL</FormLabel>
<FormControl>
<Input placeholder="+1234567890" {...field} />
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
</FormControl>
<FormDescription>
Google Chat webhook URL from your Google Chat space
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "email" && (
<>
<FormField
control={form.control}
name="email_address"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input placeholder="notifications@example.com" {...field} type="email" />
</FormControl>
<FormDescription>
Email address to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email_sender_name"
render={({ field }) => (
<FormItem>
<FormLabel>Sender Name</FormLabel>
<FormControl>
<Input placeholder="Alert System" {...field} />
</FormControl>
<FormDescription>
Display name for outgoing emails
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="smtp_server"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Server</FormLabel>
<FormControl>
<Input placeholder="smtp.gmail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_port"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Port</FormLabel>
<FormControl>
<Input placeholder="587" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="smtp_password"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Password</FormLabel>
<FormControl>
<Input placeholder="Enter your SMTP password" {...field} type="password" />
</FormControl>
<FormDescription>
Password for authenticating with the SMTP server
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "webhook" && (
<>
<FormField
control={form.control}
name="webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="https://api.example.com/webhook" {...field} />
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<FormField
control={form.control}
name="webhook_payload_template"
render={({ field }) => (
<FormItem>
<FormLabel>Payload Template (Optional)</FormLabel>
<FormDescription>
JSON template for the webhook payload. Leave empty to use default template.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Payload Templates</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="mt-4">
<h4 className="text-sm font-medium mb-2">Available Placeholders:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Server:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{server_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{cpu_usage}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{ram_usage}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{disk_usage}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Service:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{service_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{response_time}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{url}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{uptime}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">SSL:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{domain}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{expiry_date}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{days_left}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{issuer}'}</code>
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Common:</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{status}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{time}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{message}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{threshold}'}</code>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</>
)}
<FormField
control={form.control}
@@ -319,7 +637,7 @@ export const NotificationChannelDialog = ({
<div className="space-y-0.5">
<FormLabel>Enabled</FormLabel>
<FormDescription>
Turn notifications on or off
Enable or disable this notification channel
</FormDescription>
</div>
<FormControl>
@@ -338,7 +656,7 @@ export const NotificationChannelDialog = ({
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? "Update" : "Create"}
{isEditing ? "Update Channel" : "Create Channel"}
</Button>
</DialogFooter>
</form>
@@ -346,4 +664,4 @@ export const NotificationChannelDialog = ({
</DialogContent>
</Dialog>
);
};
};
@@ -13,9 +13,17 @@ import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { alertConfigService } from "@/services/alertConfigService";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
interface NotificationChannelListProps {
channels: AlertConfiguration[];
channels: CombinedChannel[];
onEdit: (config: AlertConfiguration) => void;
onDelete: (id: string) => void;
}
@@ -25,25 +33,63 @@ export const NotificationChannelList = ({
onEdit,
onDelete
}: NotificationChannelListProps) => {
const toggleEnabled = async (config: AlertConfiguration) => {
const toggleEnabled = async (config: CombinedChannel) => {
if (!config.id) return;
await alertConfigService.updateAlertConfiguration(config.id, {
enabled: !config.enabled
});
// The parent component will refresh the list
onEdit(config);
if (config.isWebhook) {
// Handle webhook toggle
try {
const newEnabled = config.enabled ? "off" : "on";
await pb.collection('webhook').update(config.id, {
enabled: newEnabled
});
// Trigger refresh by calling onEdit with empty config
onEdit({} as AlertConfiguration);
} catch (error) {
console.error("Error updating webhook:", error);
}
} else {
// Handle alert config toggle
await alertConfigService.updateAlertConfiguration(config.id, {
enabled: !config.enabled
});
// The parent component will refresh the list
onEdit(config as AlertConfiguration);
}
};
const getChannelTypeLabel = (type: string) => {
const getChannelTypeLabel = (type: string | undefined) => {
switch(type) {
case "telegram": return "Telegram";
case "discord": return "Discord";
case "slack": return "Slack";
case "signal": return "Signal";
case "google_chat": return "Google Chat";
case "email": return "Email";
default: return type;
case "webhook": return "Webhook";
default: return type || "Unknown";
}
};
const getChannelDetails = (config: CombinedChannel) => {
if (config.isWebhook) {
return `${config.method || 'POST'} ${config.url || ''}`;
}
switch(config.notification_type) {
case "telegram":
return config.telegram_chat_id || '';
case "discord":
case "slack":
case "google_chat":
return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || '';
case "signal":
return config.signal_number || '';
case "email":
return config.email_address || '';
default:
return '';
}
};
@@ -66,6 +112,7 @@ export const NotificationChannelList = ({
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
@@ -78,11 +125,14 @@ export const NotificationChannelList = ({
<TableCell>
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
</TableCell>
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
{getChannelDetails(channel)}
</TableCell>
<TableCell>
<Switch
checked={
typeof channel.enabled === 'string'
? channel.enabled === "true"
? channel.enabled === "true" || channel.enabled === "on"
: !!channel.enabled
}
onCheckedChange={() => toggleEnabled(channel)}
@@ -96,7 +146,8 @@ export const NotificationChannelList = ({
<Button
variant="outline"
size="sm"
onClick={() => onEdit(channel)}
onClick={() => onEdit(channel as AlertConfiguration)}
disabled={channel.isWebhook} // Disable edit for webhooks for now
>
<Edit className="h-4 w-4" />
</Button>
@@ -119,4 +170,4 @@ export const NotificationChannelList = ({
</Table>
</div>
);
};
};
@@ -5,28 +5,47 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Plus, Loader2 } from "lucide-react";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
import { NotificationChannelDialog } from "./NotificationChannelDialog";
import { NotificationChannelList } from "./NotificationChannelList";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
const NotificationSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [currentTab, setCurrentTab] = useState<string>("all");
const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(null);
const fetchAlertConfigurations = async () => {
const fetchNotificationChannels = async () => {
setIsLoading(true);
try {
// Fetch alert configurations
const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs);
// Fetch webhooks
try {
const webhookResponse = await pb.collection('webhook').getList(1, 50);
setWebhookConfigs(webhookResponse.items as WebhookConfiguration[]);
} catch (webhookError) {
setWebhookConfigs([]);
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchAlertConfigurations();
fetchNotificationChannels();
}, []);
const handleAddNew = () => {
@@ -40,22 +59,66 @@ const NotificationSettings = () => {
};
const handleDelete = async (id: string) => {
const success = await alertConfigService.deleteAlertConfiguration(id);
if (success) {
fetchAlertConfigurations();
// Check if it's a webhook first
const isWebhook = webhookConfigs.find(w => w.id === id);
if (isWebhook) {
// Handle webhook deletion
if (confirm("Are you sure you want to delete this webhook?")) {
try {
await pb.collection('webhook').delete(id);
fetchNotificationChannels();
} catch (error) {
console.error("Error deleting webhook:", error);
}
}
} else {
// Handle alert config deletion
const success = await alertConfigService.deleteAlertConfiguration(id);
if (success) {
fetchNotificationChannels();
}
}
};
const handleDialogClose = (refreshList: boolean) => {
setDialogOpen(false);
if (refreshList) {
fetchAlertConfigurations();
fetchNotificationChannels();
}
};
const getCombinedChannels = (): CombinedChannel[] => {
const combined: CombinedChannel[] = [];
// Add alert configurations
alertConfigs.forEach(config => {
combined.push(config);
});
// Add webhooks as notification channels
webhookConfigs.forEach(webhook => {
combined.push({
id: webhook.id,
notify_name: webhook.name,
notification_type: "webhook" as const,
enabled: webhook.enabled === "on",
created: webhook.created,
updated: webhook.updated,
isWebhook: true,
url: webhook.url,
method: webhook.method,
description: webhook.description
});
});
return combined;
};
const getFilteredConfigs = () => {
if (currentTab === "all") return alertConfigs;
return alertConfigs.filter(config => config.notification_type === currentTab);
const combined = getCombinedChannels();
if (currentTab === "all") return combined;
return combined.filter(config => config.notification_type === currentTab);
};
return (
@@ -86,7 +149,9 @@ const NotificationSettings = () => {
<TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
</TabsList>
<TabsContent value={currentTab} className="mt-0">
@@ -114,4 +179,4 @@ const NotificationSettings = () => {
);
};
export default NotificationSettings;
export default NotificationSettings;
@@ -1,24 +1,28 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { toast } from "sonner";
import { Bell } from "lucide-react";
import { Bell, X } from "lucide-react";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DialogFooter } from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { AddSSLCertificateDto } from "@/types/ssl.types";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().int().min(1).max(365),
expiry_threshold: z.coerce.number().int().min(1).max(30),
notification_channel: z.string().optional(), // Make it optional to allow empty string for "None"
notification_channels: z.array(z.string()).optional(),
alert_template: z.string().optional(),
check_interval: z.coerce.number().int().min(1).max(30).optional()
});
@@ -35,6 +39,7 @@ export const AddSSLCertificateForm = ({
}: AddSSLCertificateFormProps) => {
const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
const [isLoading, setIsLoading] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
@@ -43,58 +48,78 @@ export const AddSSLCertificateForm = ({
domain: "",
warning_threshold: 30,
expiry_threshold: 7,
notification_channel: "none",
notification_channels: [],
alert_template: "none",
check_interval: 1
}
});
// Fetch notification channels when form loads
// Fetch notification channels and SSL templates when form loads
useEffect(() => {
const fetchNotificationChannels = async () => {
const fetchData = async () => {
setIsLoading(true);
try {
// Fetch notification channels
const configs = await alertConfigService.getAlertConfigurations();
// console.log("Fetched notification channels:", configs);
// Only include enabled channels
const enabledConfigs = configs.filter(config => {
// Handle the possibility of enabled being a string
if (typeof config.enabled === 'string') {
return config.enabled === "true";
}
// Otherwise treat as boolean
return config.enabled === true;
});
setAlertConfigs(enabledConfigs);
// Fetch SSL notification templates
const templates = await sslNotificationTemplateService.getTemplates();
setSslTemplates(templates);
} catch (error) {
// console.error("Error fetching notification channels:", error);
toast.error(t('failedToLoadCertificates'));
} finally {
setIsLoading(false);
}
};
fetchNotificationChannels();
}, [form, t]);
fetchData();
}, [t]);
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
try {
// Convert the form values to the required DTO format with required properties
// Convert the form values to the required DTO format
const certData: AddSSLCertificateDto = {
domain: values.domain,
warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold,
notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string
notification_channel: values.notification_channels && values.notification_channels.length > 0
? values.notification_channels.join(',')
: '',
notification_id: values.notification_channels && values.notification_channels.length > 0
? values.notification_channels.join(',')
: '',
template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '',
check_interval: values.check_interval
};
await onSubmit(certData);
form.reset();
} catch (error) {
// console.error("Error adding SSL certificate:", error);
toast.error(t('failedToAddCertificate'));
}
};
const handleAddNotificationChannel = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
if (!currentChannels.includes(channelId)) {
form.setValue("notification_channels", [...currentChannels, channelId]);
}
};
const handleRemoveNotificationChannel = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
form.setValue("notification_channels", currentChannels.filter(id => id !== channelId));
};
const selectedChannels = form.watch("notification_channels") || [];
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
@@ -167,31 +192,60 @@ export const AddSSLCertificateForm = ({
<FormField
control={form.control}
name="notification_channel"
name="notification_channels"
render={({ field }) => (
<FormItem>
<FormLabel>{t('notificationChannel')}</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "none"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('chooseChannel')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">{t('none')}</SelectItem>
{alertConfigs.length > 0 ? (
alertConfigs.map((config) => (
<SelectItem key={config.id} value={config.id || ""}>
{config.notify_name} ({config.notification_type})
</SelectItem>
))
) : isLoading ? (
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
) : (
<SelectItem value="none-disabled" disabled>{t('noChannelsFound')}</SelectItem>
)}
</SelectContent>
</Select>
<FormLabel>{t('notificationChannel')} (Multi-select)</FormLabel>
<div className="space-y-3">
<Select
onValueChange={handleAddNotificationChannel}
value=""
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select channels to add..." />
</SelectTrigger>
</FormControl>
<SelectContent>
{alertConfigs.length > 0 ? (
alertConfigs
.filter(config => config.id && !selectedChannels.includes(config.id))
.map((config) => (
<SelectItem key={config.id} value={config.id || "unknown"}>
{config.notify_name} ({config.notification_type})
</SelectItem>
))
) : isLoading ? (
<SelectItem value="loading-placeholder" disabled>{t('loadingChannels')}</SelectItem>
) : (
<SelectItem value="no-channels-placeholder" disabled>{t('noChannelsFound')}</SelectItem>
)}
</SelectContent>
</Select>
{/* Display selected channels */}
{selectedChannels.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedChannels.map((channelId) => {
const channel = alertConfigs.find(config => config.id === channelId);
return (
<Badge key={channelId} variant="secondary" className="flex items-center gap-1">
{channel ? `${channel.notify_name} (${channel.notification_type})` : channelId}
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto p-0 ml-1"
onClick={() => handleRemoveNotificationChannel(channelId)}
>
<X className="h-3 w-3" />
</Button>
</Badge>
);
})}
</div>
)}
</div>
<FormDescription className="flex items-center gap-1">
<Bell className="h-4 w-4" />
{t('whereToSend')}
@@ -200,6 +254,41 @@ export const AddSSLCertificateForm = ({
</FormItem>
)}
/>
<FormField
control={form.control}
name="alert_template"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Template</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "none"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Choose an alert template (optional)" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{sslTemplates.length > 0 ? (
sslTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))
) : isLoading ? (
<SelectItem value="loading-templates" disabled>Loading templates...</SelectItem>
) : (
<SelectItem value="no-templates-found" disabled>No templates found</SelectItem>
)}
</SelectContent>
</Select>
<FormDescription>
Template for SSL certificate alert messages
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={onCancel}>{t('cancel')}</Button>
@@ -7,17 +7,20 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Badge } from "@/components/ui/badge";
import { SSLCertificate } from "@/types/ssl.types";
import { Loader2, Bell } from "lucide-react";
import { Loader2, Bell, X } from "lucide-react";
import { toast } from "sonner";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
notification_channel: z.string().min(1, "Notification channel is required"),
notification_channels: z.array(z.string()).default([]),
alert_template: z.string().optional(),
check_interval: z.coerce.number().int().min(1).max(30).optional(),
});
@@ -33,66 +36,138 @@ interface EditSSLCertificateFormProps {
export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => {
const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
const [isLoading, setIsLoading] = useState(false);
// Parse existing notification channels from certificate data
const parseNotificationChannels = (cert: SSLCertificate): string[] => {
const channels: string[] = [];
// Check notification_id field first (multi-channel support)
if (cert.notification_id && cert.notification_id.trim()) {
channels.push(...cert.notification_id.split(',').map(id => id.trim()).filter(Boolean));
}
// Fallback to notification_channel field
else if (cert.notification_channel && cert.notification_channel.trim()) {
channels.push(...cert.notification_channel.split(',').map(id => id.trim()).filter(Boolean));
}
return channels;
};
// Get the alert template from certificate data
const getAlertTemplate = (cert: SSLCertificate): string => {
let templateId = "";
// Check template_id field first (new field for PocketBase)
if (cert.template_id && cert.template_id.trim()) {
templateId = cert.template_id;
}
// Fallback to alert_template field
else if (cert.alert_template && cert.alert_template.trim()) {
templateId = cert.alert_template;
}
return templateId || "none";
};
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
domain: certificate.domain,
warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold,
notification_channel: certificate.notification_channel,
notification_channels: parseNotificationChannels(certificate),
alert_template: getAlertTemplate(certificate),
check_interval: certificate.check_interval || 1,
},
});
// Fetch notification channels when form loads
const notificationChannels = form.watch("notification_channels") || [];
// Fetch notification channels and SSL templates when form loads
useEffect(() => {
const fetchNotificationChannels = async () => {
const fetchData = async () => {
setIsLoading(true);
try {
// Fetch notification channels
const configs = await alertConfigService.getAlertConfigurations();
// console.log("Fetched notification channels:", configs);
// Only include enabled channels
const enabledConfigs = configs.filter(config => {
// Handle the possibility of enabled being a string
if (typeof config.enabled === 'string') {
return config.enabled === "true";
}
// Otherwise treat as boolean
return config.enabled === true;
});
setAlertConfigs(enabledConfigs);
// Fetch SSL notification templates
const templates = await sslNotificationTemplateService.getTemplates();
setSslTemplates(templates);
} catch (error) {
// console.error("Error fetching notification channels:", error);
toast.error(t('failedToLoadCertificates'));
} finally {
setIsLoading(false);
}
};
fetchNotificationChannels();
fetchData();
}, [t]);
const handleSubmit = (data: FormValues) => {
// Update form values when certificate data changes
useEffect(() => {
if (certificate) {
const channels = parseNotificationChannels(certificate);
const template = getAlertTemplate(certificate);
form.reset({
domain: certificate.domain,
warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold,
notification_channels: channels,
alert_template: template,
check_interval: certificate.check_interval || 1,
});
}
}, [certificate, form]);
const handleChannelAdd = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
if (!currentChannels.includes(channelId)) {
const newChannels = [...currentChannels, channelId];
form.setValue("notification_channels", newChannels);
}
};
const handleChannelRemove = (channelId: string) => {
const currentChannels = form.getValues("notification_channels") || [];
const newChannels = currentChannels.filter(id => id !== channelId);
form.setValue("notification_channels", newChannels);
};
const getSelectedChannelNames = () => {
return (notificationChannels || []).map(channelId => {
const config = alertConfigs.find(c => c.id === channelId);
return config ? `${config.notify_name} (${config.notification_type})` : channelId;
});
};
const handleSubmit = (data: FormValues) => {
// Merge the updated values with the original certificate
const updatedCertificate: SSLCertificate = {
...certificate,
...data,
// Ensure values are correctly typed as numbers
notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '',
warning_threshold: Number(data.warning_threshold),
expiry_threshold: Number(data.expiry_threshold),
check_interval: data.check_interval ? Number(data.check_interval) : undefined
};
console.log("Submitting updated certificate:", updatedCertificate);
onSubmit(updatedCertificate);
};
// For debugging
console.log("Certificate data:", certificate);
console.log("Form default values:", form.getValues());
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
@@ -106,7 +181,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
<Input
{...field}
placeholder="example.com"
disabled={true} // Domain shouldn't be editable
disabled={true}
/>
</FormControl>
<FormDescription>
@@ -188,41 +263,52 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
<FormField
control={form.control}
name="notification_channel"
name="notification_channels"
render={({ field }) => (
<FormItem>
<FormLabel>{t('notificationChannel')}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
disabled={isLoading}
>
<FormControl>
<FormDescription>
Select multiple notification channels for this SSL certificate
</FormDescription>
{/* Display selected channels as badges */}
{notificationChannels && notificationChannels.length > 0 && (
<div className="flex flex-wrap gap-2 mb-2">
{getSelectedChannelNames().map((channelName, index) => (
<Badge key={notificationChannels[index]} variant="secondary" className="flex items-center gap-1">
{channelName}
<X
className="h-3 w-3 cursor-pointer"
onClick={() => handleChannelRemove(notificationChannels[index])}
/>
</Badge>
))}
</div>
)}
<FormControl>
<Select
onValueChange={handleChannelAdd}
disabled={isLoading}
value=""
>
<SelectTrigger>
<SelectValue placeholder={t('chooseChannel')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{alertConfigs.length > 0 ? (
alertConfigs.map((config) => (
<SelectItem key={config.id} value={config.id || ""}>
{config.notify_name} ({config.notification_type})
</SelectItem>
))
) : isLoading ? (
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
) : (
<>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="slack">Slack</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
<SelectItem value="none">None</SelectItem>
</>
)}
</SelectContent>
</Select>
<SelectContent>
{alertConfigs
.filter(config => config.id && !notificationChannels?.includes(config.id))
.map((config) => (
<SelectItem key={config.id} value={config.id || "unknown"}>
{config.notify_name} ({config.notification_type})
</SelectItem>
))}
{alertConfigs.filter(config => config.id && !notificationChannels?.includes(config.id)).length === 0 && (
<SelectItem value="no-available-channels" disabled>No available channels</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormDescription className="flex items-center gap-1">
<Bell className="h-4 w-4" />
{t('whereToSend')}
@@ -232,6 +318,45 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
)}
/>
<FormField
control={form.control}
name="alert_template"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Template</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value || "none"}
disabled={isLoading}
>
<SelectTrigger>
<SelectValue placeholder={isLoading ? "Loading templates..." : "Select an alert template"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{sslTemplates?.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
{sslTemplates?.length === 0 && !isLoading && (
<SelectItem value="no-templates-available" disabled>No templates found</SelectItem>
)}
{isLoading && (
<SelectItem value="loading-templates-placeholder" disabled>Loading templates...</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Choose a template for SSL certificate alert messages
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-3 pt-4">
<Button
type="button"
@@ -45,20 +45,20 @@ export const SSLCertificateActions = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onView(certificate)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onView(certificate); }}>
<Eye className="mr-2 h-4 w-4" />
{t('view')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCheck}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleCheck(); }}>
<RefreshCw className="mr-2 h-4 w-4" />
Check
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEdit(certificate)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onEdit(certificate); }}>
<Edit className="mr-2 h-4 w-4" />
{t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(certificate)}
onClick={(e) => { e.stopPropagation(); onDelete(certificate); }}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -67,4 +67,4 @@ export const SSLCertificateActions = ({
</DropdownMenuContent>
</DropdownMenu>
);
};
};
@@ -29,6 +29,21 @@ export const SSLCertificateDetailDialog = ({
if (!certificate) return null;
// Parse Subject Alternative Names for better display
const formatSANs = (sans: string): string[] => {
if (!sans) return [];
// Split by common delimiters and clean up
const sansList = sans
.split(/[,;\n]/)
.map(san => san.trim())
.filter(san => san.length > 0);
return sansList;
};
const sansList = certificate.cert_sans ? formatSANs(certificate.cert_sans) : [];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
@@ -135,7 +150,17 @@ export const SSLCertificateDetailDialog = ({
<div className="mt-4">
<p className="text-sm font-medium text-muted-foreground mb-2">{t('subjectAlternativeNames')}</p>
<div className="bg-muted px-3 py-2 rounded">
<p className="text-sm whitespace-pre-wrap">{certificate.cert_sans}</p>
{sansList.length > 0 ? (
<div className="space-y-1">
{sansList.map((san, index) => (
<div key={index} className="text-sm font-mono break-all">
{san}
</div>
))}
</div>
) : (
<p className="text-sm">N/A</p>
)}
</div>
</div>
)}
@@ -33,11 +33,13 @@ import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import { SSLCertificateActions } from "./SSLCertificateActions";
import { SSLCertificateDetailDialog } from "./SSLCertificateDetailDialog";
import { SSLPagination } from "./SSLPagination";
import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
import { pb } from "@/lib/pocketbase";
import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext";
import { useTheme } from "@/contexts/ThemeContext";
import { useSSLPagination } from "@/hooks/useSSLPagination";
import { toast } from "sonner";
export const SSLCertificatesTable = () => {
@@ -56,6 +58,16 @@ export const SSLCertificatesTable = () => {
queryFn: fetchSSLCertificates,
});
const {
paginatedCertificates,
currentPage,
totalPages,
pageSize,
totalItems,
handlePageChange,
handlePageSizeChange,
} = useSSLPagination({ certificates });
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading certificates</div>;
@@ -67,7 +79,6 @@ export const SSLCertificatesTable = () => {
setShowAddDialog(false);
toast.success(t('certificateAdded'));
} catch (error) {
// console.error("Error adding certificate:", error);
toast.error(t('failedToAddCertificate'));
} finally {
setIsSubmitting(false);
@@ -89,7 +100,6 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateUpdated'));
} catch (error) {
// console.error("Error updating certificate:", error);
toast.error(t('failedToUpdateCertificate'));
} finally {
setIsSubmitting(false);
@@ -107,7 +117,6 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateDeleted'));
} catch (error) {
// console.error("Error deleting certificate:", error);
toast.error(t('failedToDeleteCertificate'));
} finally {
setIsSubmitting(false);
@@ -136,53 +145,68 @@ export const SSLCertificatesTable = () => {
{t('noCertificatesFound')}
</div>
) : (
<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`}>{t('domain')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.map((certificate) => (
<TableRow key={certificate.id} className="hover:bg-muted/50">
<TableCell className="font-medium">
{certificate.domain}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell>
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
</TableCell>
<TableCell>
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
{certificate.days_left} {t('days')}
</span>
</TableCell>
<TableCell>
{certificate.check_interval || 1} {t('days')}
</TableCell>
<TableCell className="text-right">
<SSLCertificateActions
certificate={certificate}
onView={openViewDialog}
onEdit={openEditDialog}
onDelete={openDeleteDialog}
/>
</TableCell>
<>
<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`}>{t('domain')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{paginatedCertificates.map((certificate) => (
<TableRow
key={certificate.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => openViewDialog(certificate)}
>
<TableCell className="font-medium">
{certificate.domain}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell>
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
</TableCell>
<TableCell>
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
{certificate.days_left} {t('days')}
</span>
</TableCell>
<TableCell>
{certificate.check_interval || 1} {t('days')}
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<SSLCertificateActions
certificate={certificate}
onView={openViewDialog}
onEdit={openEditDialog}
onDelete={openDeleteDialog}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<SSLPagination
currentPage={currentPage}
totalPages={totalPages}
pageSize={pageSize}
totalItems={totalItems}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</>
)}
{/* View Certificate Dialog */}
@@ -1,4 +1,3 @@
import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@@ -30,12 +29,9 @@ export const SSLDomainContent = () => {
queryKey: ['ssl-certificates'],
queryFn: async () => {
try {
console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates();
console.log("Received SSL certificates:", result);
return result;
} catch (error) {
console.error("Error fetching certificates:", error);
toast.error(t('failedToLoadCertificates'));
throw error;
}
@@ -53,30 +49,28 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateAdded'));
},
onError: (error) => {
console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
}
});
// Edit certificate mutation - Updated to ensure thresholds are properly updated
// Edit certificate mutation - Updated to include notification_id and template_id
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
console.log("Updating certificate with data:", certificate);
// Create the update data object
// 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
check_interval: certificate.check_interval,
};
console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
console.log("PocketBase update response:", updated);
// 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
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
@@ -90,7 +84,6 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateUpdated'));
},
onError: (error) => {
console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
});
@@ -103,7 +96,6 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateDeleted'));
},
onError: (error) => {
console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
});
@@ -117,7 +109,6 @@ export const SSLDomainContent = () => {
// Removed individual success toast notification
},
onError: (error) => {
console.error("Error refreshing SSL certificate:", error);
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
@@ -142,7 +133,6 @@ export const SSLDomainContent = () => {
}
},
onError: (error) => {
console.error("Error refreshing all certificates:", error);
toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false);
@@ -167,7 +157,6 @@ export const SSLDomainContent = () => {
};
const handleUpdateCertificate = (certificate: SSLCertificate) => {
console.log("Handling certificate update with data:", certificate);
editMutation.mutate(certificate);
};
@@ -0,0 +1,131 @@
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { SSLPageSize } from "@/hooks/useSSLPagination";
import { useLanguage } from "@/contexts/LanguageContext";
interface SSLPaginationProps {
currentPage: number;
totalPages: number;
pageSize: SSLPageSize;
totalItems: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: SSLPageSize) => void;
}
export function SSLPagination({
currentPage,
totalPages,
pageSize,
totalItems,
onPageChange,
onPageSizeChange,
}: SSLPaginationProps) {
const { t } = useLanguage();
// Generate page numbers to display
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
// Adjust start page if we don't have enough pages at the end
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const startItem = Math.min((currentPage - 1) * pageSize + 1, totalItems);
const endItem = Math.min(currentPage * pageSize, totalItems);
return (
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">
{t("rowsPerPage") || "Rows per page"}:
</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => onPageSizeChange(parseInt(value) as SSLPageSize)}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="30">30</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
{totalItems > 0
? `${startItem}-${endItem} of ${totalItems} ${t("certificates") || "certificates"}`
: `0 ${t("certificates") || "certificates"}`
}
</span>
</div>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{getPageNumbers().map((page) => (
<PaginationItem key={page}>
<PaginationLink
isActive={page === currentPage}
onClick={() => onPageChange(page)}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { useState, useMemo } from 'react';
import { SSLCertificate } from '@/types/ssl.types';
export type SSLPageSize = 10 | 30 | 50;
interface UseSSLPaginationProps {
certificates: SSLCertificate[];
initialPageSize?: SSLPageSize;
}
export const useSSLPagination = ({
certificates,
initialPageSize = 10
}: UseSSLPaginationProps) => {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState<SSLPageSize>(initialPageSize);
const { paginatedCertificates, totalPages } = useMemo(() => {
const totalItems = certificates.length;
const pages = Math.ceil(totalItems / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return {
paginatedCertificates: certificates.slice(startIndex, endIndex),
totalPages: Math.max(1, pages)
};
}, [certificates, currentPage, pageSize]);
// Reset to first page when page size changes or certificates change
const handlePageSizeChange = (newPageSize: SSLPageSize) => {
setPageSize(newPageSize);
setCurrentPage(1);
};
// Reset to first page if current page exceeds total pages
const handlePageChange = (page: number) => {
const validPage = Math.min(Math.max(1, page), totalPages);
setCurrentPage(validPage);
};
return {
paginatedCertificates,
currentPage,
totalPages,
pageSize,
totalItems: certificates.length,
handlePageChange,
handlePageSizeChange,
};
};
+55 -85
View File
@@ -1,18 +1,14 @@
import { useState, useEffect } from 'react';
import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService } from '@/services/authService';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui/use-toast';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react";
import { Eye, EyeOff, Mail, LogIn, AlertCircle, Github } from "lucide-react";
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { ForgotPasswordDialog } from '@/components/auth/ForgotPasswordDialog';
const Login = () => {
@@ -20,30 +16,13 @@ const Login = () => {
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [currentEndpoint, setCurrentEndpoint] = useState(getCurrentEndpoint());
const [showForgotPassword, setShowForgotPassword] = useState(false);
const [loginError, setLoginError] = useState('');
const navigate = useNavigate();
const { t } = useLanguage();
const { theme } = useTheme();
// Add responsiveness check
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 640);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => {
window.removeEventListener('resize', checkScreenSize);
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setLoginError(''); // Clear previous errors
@@ -69,7 +48,7 @@ const Login = () => {
setLoginError(error.message);
}
} else {
setLoginError(`${t("authenticationFailed")}. Server: ${currentEndpoint}`);
setLoginError(t("authenticationFailed"));
}
toast({
@@ -82,60 +61,35 @@ const Login = () => {
error.message.includes('invalid email or password'))
? t("invalidCredentials")
: error.message
: `${t("authenticationFailed")}. Server: ${currentEndpoint}`,
: t("authenticationFailed"),
});
} finally {
setLoading(false);
}
};
}, [email, password, navigate, t]);
const handleEndpointChange = (value: string) => {
setCurrentEndpoint(value);
setApiEndpoint(value);
};
const togglePasswordVisibility = useCallback(() => {
setShowPassword(prev => !prev);
}, []);
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setLoginError(''); // Clear error when user starts typing
}, []);
const handlePasswordChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
setLoginError(''); // Clear error when user starts typing
}, []);
const openExternalLink = useCallback((url: string) => {
window.open(url, '_blank');
}, []);
return (
<div className="flex items-center justify-center min-h-screen bg-background p-4">
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-4">
<div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20">
<div className="text-center relative">
{/* Commented out API Endpoint Settings button
<div className="absolute right-0 top-0">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" aria-label="API Settings">
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] sm:w-80">
<div className="space-y-4">
<h4 className="font-medium">API Endpoint Settings</h4>
<RadioGroup
value={currentEndpoint}
onValueChange={handleEndpointChange}
className="gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.LOCAL} id="local" />
<Label htmlFor="local" className="truncate text-sm">Local: {API_ENDPOINTS.LOCAL}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.REMOTE} id="remote" />
<Label htmlFor="remote" className="truncate text-sm">Remote: {API_ENDPOINTS.REMOTE}</Label>
</div>
</RadioGroup>
<div className="text-xs text-muted-foreground truncate">
Current endpoint: {currentEndpoint}
</div>
</div>
</PopoverContent>
</Popover>
</div>
*/}
<div className="text-center">
{/* Logo */}
<div className="mb-4">
<img
@@ -146,11 +100,8 @@ const Login = () => {
</div>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
{/* Removed "Don't have an account? Create one" text */}
</div>
{/* Removed Google Sign in button section */}
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
{/* Error Alert */}
{loginError && (
@@ -171,10 +122,7 @@ const Login = () => {
placeholder="your.email@provider.com"
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setLoginError(''); // Clear error when user starts typing
}}
onChange={handleEmailChange}
required
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
/>
@@ -204,10 +152,7 @@ const Login = () => {
placeholder="••••••••••••"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => {
setPassword(e.target.value);
setLoginError(''); // Clear error when user starts typing
}}
onChange={handlePasswordChange}
required
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
/>
@@ -234,13 +179,38 @@ const Login = () => {
{loading ? t("signingIn") : t("signIn")}
{!loading && <LogIn className="ml-2 h-4 w-4" />}
</Button>
<p className="text-xxs sm:text-xs text-center text-muted-foreground">
{t("bySigningIn")} <a href="#" className="text-emerald-500">{t("termsAndConditions")}</a> {t("and")} <a href="#" className="text-emerald-500">{t("privacyPolicy")}</a>.
</p>
</form>
</div>
{/* Footer with Social Media Icons */}
<div className="mt-8 flex items-center justify-center space-x-6">
<button
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="GitHub"
>
<Github className="h-5 w-5" />
</button>
<button
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="X (Twitter)"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</button>
<button
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Discord"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
</button>
</div>
<ForgotPasswordDialog
open={showForgotPassword}
onOpenChange={setShowForgotPassword}
+76 -20
View File
@@ -7,28 +7,39 @@ export interface AlertConfiguration {
collectionId?: string;
collectionName?: string;
service_id: string;
notification_type: "telegram" | "discord" | "signal" | "slack" | "email";
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
telegram_chat_id?: string;
discord_webhook_url?: string;
signal_number?: string;
signal_api_endpoint?: string;
notify_name: string;
bot_token?: string;
template_id?: string;
slack_webhook_url?: string;
google_chat_webhook_url?: string;
enabled: boolean;
created?: string;
updated?: string;
// Email specific fields
email_address?: string;
email_sender_name?: string;
smtp_server?: string;
smtp_port?: string;
smtp_password?: string;
webhook_id?: string;
channel_id?: string;
// Webhook fields for alert_configurations
webhook_url?: string;
webhook_payload_template?: string;
}
export const alertConfigService = {
async getAlertConfigurations(): Promise<AlertConfiguration[]> {
// console.info("Fetching alert configurations");
try {
const response = await pb.collection('alert_configurations').getList(1, 50);
// console.info("Alert configurations response:", response);
return response.items as AlertConfiguration[];
} catch (error) {
// console.error("Error fetching alert configurations:", error);
toast({
title: "Error",
description: "Failed to load notification settings",
@@ -39,20 +50,61 @@ export const alertConfigService = {
},
async createAlertConfiguration(config: Omit<AlertConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<AlertConfiguration | null> {
// console.info("Creating alert configuration:", config);
try {
const result = await pb.collection('alert_configurations').create(config);
// console.info("Alert configuration created:", result);
// Build the configuration object with proper field mapping
const cleanConfig: any = {
service_id: config.service_id || "global",
notification_type: config.notification_type,
notify_name: config.notify_name,
enabled: config.enabled,
template_id: config.template_id || "",
};
// Add type-specific fields based on notification type
if (config.notification_type === "telegram") {
cleanConfig.telegram_chat_id = config.telegram_chat_id || "";
cleanConfig.bot_token = config.bot_token || "";
} else if (config.notification_type === "discord") {
cleanConfig.discord_webhook_url = config.discord_webhook_url || "";
} else if (config.notification_type === "slack") {
cleanConfig.slack_webhook_url = config.slack_webhook_url || "";
} else if (config.notification_type === "signal") {
cleanConfig.signal_number = config.signal_number || "";
cleanConfig.signal_api_endpoint = config.signal_api_endpoint || "";
} else if (config.notification_type === "google_chat") {
cleanConfig.google_chat_webhook_url = config.google_chat_webhook_url || "";
} else if (config.notification_type === "email") {
cleanConfig.email_address = config.email_address || "";
cleanConfig.email_sender_name = config.email_sender_name || "";
cleanConfig.smtp_server = config.smtp_server || "";
cleanConfig.smtp_port = config.smtp_port || "";
cleanConfig.smtp_password = config.smtp_password || "";
} else if (config.notification_type === "webhook") {
cleanConfig.webhook_url = config.webhook_url || "";
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
}
const result = await pb.collection('alert_configurations').create(cleanConfig);
toast({
title: "Success",
description: "Notification settings saved successfully",
description: "Notification channel created successfully",
});
return result as AlertConfiguration;
} catch (error) {
// console.error("Error creating alert configuration:", error);
// Try to get more details from the error
if (error && typeof error === 'object') {
}
toast({
title: "Error",
description: "Failed to save notification settings",
description: "Failed to create notification channel",
variant: "destructive"
});
return null;
@@ -60,20 +112,27 @@ export const alertConfigService = {
},
async updateAlertConfiguration(id: string, config: Partial<AlertConfiguration>): Promise<AlertConfiguration | null> {
// console.info(`Updating alert configuration ${id}:`, config);
try {
const result = await pb.collection('alert_configurations').update(id, config);
// console.info("Alert configuration updated:", result);
// Build the update config with proper field mapping
const updateConfig: any = {};
// Copy all provided fields
Object.keys(config).forEach(key => {
if (config[key as keyof AlertConfiguration] !== undefined) {
updateConfig[key] = config[key as keyof AlertConfiguration];
}
});
const result = await pb.collection('alert_configurations').update(id, updateConfig);
toast({
title: "Success",
description: "Notification settings updated successfully",
description: "Notification channel updated successfully",
});
return result as AlertConfiguration;
} catch (error) {
// console.error("Error updating alert configuration:", error);
toast({
title: "Error",
description: "Failed to update notification settings",
description: "Failed to update notification channel",
variant: "destructive"
});
return null;
@@ -81,17 +140,14 @@ export const alertConfigService = {
},
async deleteAlertConfiguration(id: string): Promise<boolean> {
// console.info(`Deleting alert configuration ${id}`);
try {
await pb.collection('alert_configurations').delete(id);
// console.info("Alert configuration deleted");
toast({
title: "Success",
description: "Notification channel removed",
});
return true;
} catch (error) {
// console.error("Error deleting alert configuration:", error);
toast({
title: "Error",
description: "Failed to remove notification channel",
@@ -100,4 +156,4 @@ export const alertConfigService = {
return false;
}
}
};
};
@@ -1,7 +1,6 @@
import { pb } from '@/lib/pocketbase';
import { uptimeService } from '@/services/uptimeService';
import { notificationService } from '@/services/notification'; // Import from the main notification service
import { prepareServiceForNotification } from '../utils/httpUtils';
import { UptimeData } from '@/types/service.types';
@@ -9,7 +8,7 @@ import { UptimeData } from '@/types/service.types';
* Handle a service that is determined to be UP
*/
export async function handleServiceUp(service: any, responseTime: number, formattedTime: string): Promise<void> {
console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`);
// console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`);
// Create a history record of this check with a more accurate timestamp
const uptimeData: UptimeData = {
@@ -41,47 +40,18 @@ export async function handleServiceUp(service: any, responseTime: number, format
try {
await uptimeService.recordUptimeData(uptimeData);
} catch (error) {
console.error("Failed to record uptime data on first try, retrying...", error);
// console.error("Failed to record uptime data on first try, retrying...", error);
// Wait a short time and retry once
await new Promise(resolve => setTimeout(resolve, 1000));
await uptimeService.recordUptimeData(uptimeData);
}
// Reset notification count if service is recovered from DOWN status
if (previousStatus === "down") {
console.log(`Service ${service.name} recovered from DOWN status - resetting notification count`);
notificationService.resetNotificationCount(service.id);
}
// Send notification if status changed from down to up
// Status change logging (notification logic removed - will be handled by backend)
if (statusChanged) {
console.log(`Status changed from ${previousStatus} to UP - sending notification`);
// Convert PocketBase record to Service type for notification
const serviceForNotification = prepareServiceForNotification(service, "up", responseTime);
// Check if alerts are muted - STRICT check for "muted" string value
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING UP notification`);
return;
}
try {
// Send notification through main notification service
await notificationService.sendNotification({
service: serviceForNotification,
status: "up",
responseTime: responseTime,
timestamp: new Date().toISOString()
});
console.log("UP notification sent successfully");
} catch (error) {
console.error("Error sending UP notification:", error);
}
// console.log(`Status changed from ${previousStatus} to UP - notification will be handled by backend`);
}
} catch (error) {
console.error("Error handling service UP state:", error);
// console.error("Error handling service UP state:", error);
}
}
@@ -89,7 +59,7 @@ export async function handleServiceUp(service: any, responseTime: number, format
* Handle a service that is determined to be DOWN
*/
export async function handleServiceDown(service: any, formattedTime: string): Promise<void> {
console.log(`Service ${service.name} is DOWN!`);
// console.log(`Service ${service.name} is DOWN!`);
// Create a history record of this check
const uptimeData: UptimeData = {
@@ -106,7 +76,7 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
const previousStatus = service.status;
const statusChanged = previousStatus !== "down";
console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`);
// console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`);
try {
// Update service status
@@ -123,44 +93,15 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
try {
await uptimeService.recordUptimeData(uptimeData);
} catch (error) {
console.error("Failed to record uptime data on first try, retrying...", error);
// console.error("Failed to record uptime data on first try, retrying...", error);
// Wait a short time and retry once
await new Promise(resolve => setTimeout(resolve, 1000));
await uptimeService.recordUptimeData(uptimeData);
}
// Convert PocketBase record to Service type for notification
const serviceForNotification = prepareServiceForNotification(service, "down");
// Check if alerts are muted - STRICT check for "muted" string value
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING DOWN notification`);
return;
}
console.log("Attempting to send DOWN notification for service:", service.name);
console.log("Service notification data:", {
name: serviceForNotification.name,
notificationChannel: serviceForNotification.notificationChannel,
alertTemplate: serviceForNotification.alertTemplate,
retries: serviceForNotification.retries || 3,
alerts: serviceForNotification.alerts
});
try {
// Use the main notification service - this handles retries internally
const result = await notificationService.sendNotification({
service: serviceForNotification,
status: "down",
responseTime: 0,
timestamp: new Date().toISOString()
});
console.log("DOWN notification sent result:", result);
} catch (error) {
console.error("Error sending DOWN notification:", error);
}
// Status change logging (notification logic removed - will be handled by backend)
// console.log("Service DOWN status recorded - notification will be handled by backend");
} catch (error) {
console.error("Error handling service DOWN state:", error);
// console.error("Error handling service DOWN state:", error);
}
}
@@ -13,7 +13,7 @@ export async function pauseMonitoring(serviceId: string): Promise<void> {
if (intervalId) {
clearInterval(intervalId);
monitoringIntervals.delete(serviceId);
console.log(`Monitoring paused for service ${serviceId}`);
// console.log(`Monitoring paused for service ${serviceId}`);
}
// Get current timestamp formatted as a string
@@ -1,7 +1,6 @@
import { pb } from '@/lib/pocketbase';
import { monitoringIntervals } from '../monitoringIntervals';
import { notificationService } from '@/services/notificationService';
import { Service } from '@/types/service.types';
import { startMonitoringService } from './startMonitoring';
@@ -16,7 +15,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
// Fetch the current service to get its name for better logging
const service = await pb.collection('services').getOne(serviceId);
// console.log(`Resuming service ${service.name} at ${now}`);
// console.log(`Resuming service ${service.name} at ${now}`);
// First, clear any existing interval just to be safe
const existingInterval = monitoringIntervals.get(serviceId);
@@ -32,50 +31,14 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
last_checked: now
});
// Convert PocketBase record to Service type for notification
const serviceForNotification: Service = {
id: service.id,
name: service.name,
url: service.url || "",
host: service.host || "", // Include host property
type: service.service_type || service.type || "HTTP",
status: "up",
responseTime: service.response_time || 0,
uptime: service.uptime || 0,
lastChecked: now,
interval: service.heartbeat_interval || 60,
retries: service.max_retries || 3,
notificationChannel: service.notification_id,
alertTemplate: service.template_id,
muteAlerts: service.mute_alerts || false,
alerts: service.alerts || "unmuted",
muteChangedAt: service.mute_changed_at,
};
// Double check if alerts are muted before sending notification
// Ensure we're using the correct field - alerts should be "muted" or "unmuted"
const alertsMuted = service.alerts === "muted" || serviceForNotification.alerts === "muted";
if (!alertsMuted) {
// console.log(`Alerts NOT muted for service ${service.name}, sending resume notification`);
// Send notification that service has been resumed
await notificationService.sendNotification({
service: serviceForNotification,
status: "up",
timestamp: now
});
} else {
// console.log(`Alerts muted for service ${service.name}, skipping resume notification`);
}
// IMPORTANT: Wait a brief moment to ensure the status update is processed
await new Promise(resolve => setTimeout(resolve, 500));
// Now start the service monitoring with a clean slate
await startMonitoringService(serviceId);
console.log(`Service ${service.name} resumed and ready for monitoring`);
// console.log(`Service ${service.name} resumed and ready for monitoring`);
} catch (error) {
// console.error("Error resuming service:", error);
// console.error("Error resuming service:", error);
}
}
+4 -232
View File
@@ -1,234 +1,6 @@
import { pb } from "@/lib/pocketbase";
import { toast } from "@/hooks/use-toast";
import { AlertConfiguration } from "../alertConfigService";
import { NotificationTemplate } from "../templateService";
import { NotificationPayload } from "./types";
import { processTemplate, generateDefaultMessage } from "./templateProcessor";
import { sendTelegramNotification } from "./telegramService";
import { sendSignalNotification } from "./signalService";
// Track last notification times for services to implement cooldown
const lastNotifications: Record<string, {
timestamp: Date;
count: number;
lastMessageTime: Date; // Track when the last message was actually sent
}> = {};
// Re-export template processor only
export { processTemplate, generateDefaultMessage } from './templateProcessor';
// Cooldown period in milliseconds (5 minutes)
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
/**
* Notification service responsible for sending notifications based on service status changes
*/
export const notificationService = {
/**
* Send notification based on service status change
*/
async sendNotification(payload: NotificationPayload): Promise<boolean> {
try {
console.log("=== NOTIFICATION SERVICE TRIGGER ===");
console.log("Sending notification for service:", payload.service.name, "Status:", payload.status);
console.log("Service type:", payload.service.type);
console.log("Service retries:", payload.service.retries || 3);
console.log("Alerts status:", payload.service.alerts);
// First check: Strictly check if alerts are muted for this specific service
if (payload.service.alerts === "muted") {
console.log(`BLOCKING NOTIFICATION: Alerts are explicitly muted for service ${payload.service.name} (ID: ${payload.service.id})`);
return true; // Return true as this is expected behavior, not a failure
}
// For DOWN status, implement the cooldown and max retries logic
if (payload.status === "down") {
const serviceId = payload.service.id;
// Get max retries from service config or default to 3
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
const now = new Date();
console.log(`DOWN notification for ${payload.service.name} - Max retries: ${maxRetries}`);
if (lastNotifications[serviceId]) {
const lastNotif = lastNotifications[serviceId];
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
console.log(`Time since last notification tracking for ${payload.service.name}: ${Math.round(timeSinceLastNotif/1000)}s`);
console.log(`Current notification count: ${lastNotif.count}/${maxRetries}`);
// Check if we're within the cooldown period
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
// If max retries is set to 1, we should only send one notification during the cooldown period
if (maxRetries === 1) {
console.log(`Max retries is 1 - BLOCKING duplicate DOWN notification for ${payload.service.name}`);
return true; // Skip notification but return success
}
// Check if we've reached max retries
if (lastNotif.count >= maxRetries) {
console.log(`DOWN notification for ${payload.service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
return true; // Skip notification but return success
}
// Increment count since we're sending another notification
console.log(`DOWN notification for ${payload.service.name}: ${lastNotif.count + 1}/${maxRetries}`);
lastNotifications[serviceId].count += 1;
lastNotifications[serviceId].lastMessageTime = now;
} else {
// Reset count after cooldown period
console.log(`Cooldown period elapsed for ${payload.service.name}. Resetting notification count.`);
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
}
} else {
// First notification for this service
console.log(`First DOWN notification for ${payload.service.name}: 1/${maxRetries}`);
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
}
}
// Check if service has notification channel configured
const notificationChannel = payload.service.notificationChannel;
if (!notificationChannel || notificationChannel === "none") {
console.log("No notification channel configured for service", payload.service.name);
return false;
}
console.log("Using notification channel ID:", notificationChannel);
try {
// Get the alert configuration details
console.log("Fetching alert configuration with ID:", notificationChannel);
const alertConfig = await pb.collection('alert_configurations').getOne(notificationChannel);
console.log("Found alert configuration:", JSON.stringify({
...alertConfig,
bot_token: alertConfig.bot_token ? "[REDACTED]" : undefined
}, null, 2));
// Check enabled status - skip if explicitly disabled
if (alertConfig.enabled === false || alertConfig.enabled === "false") {
console.log("Alert configuration is disabled, skipping notification");
return false;
}
// Prepare message content
let messageContent = await this.prepareMessageContent(
payload.service,
payload.status,
payload.responseTime,
payload.message
);
// For DOWN status, add retry information to the message
if (payload.status === "down" && lastNotifications[payload.service.id]) {
const retryInfo = lastNotifications[payload.service.id];
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
if (maxRetries > 1) {
messageContent += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
}
}
console.log(`Prepared message for ${payload.status} notification:`, messageContent);
// Send notification based on type
if (alertConfig.notification_type === "telegram") {
console.log("Sending Telegram notification for service status change");
return await sendTelegramNotification(alertConfig as AlertConfiguration, messageContent);
} else if (alertConfig.notification_type === "signal") {
console.log("Sending Signal notification with message:", messageContent);
return await sendSignalNotification(alertConfig as AlertConfiguration, messageContent);
}
// Add other notification types here
console.log("Notification type not supported:", alertConfig.notification_type);
toast({
title: "Notification Error",
description: `Notification type '${alertConfig.notification_type}' not supported`,
variant: "destructive"
});
return false;
} catch (error) {
console.error("Error processing notification channel:", error);
console.error("Error details:", error instanceof Error ? error.message : "Unknown error");
toast({
title: "Notification Error",
description: `Error processing notification: ${error instanceof Error ? error.message : "Unknown error"}`,
variant: "destructive"
});
return false;
}
} catch (error) {
console.error("Error sending notification:", error);
toast({
title: "Notification Error",
description: `Failed to send notification: ${error instanceof Error ? error.message : "Unknown error"}`,
variant: "destructive"
});
return false;
}
},
/**
* Prepare message content for notification
*/
async prepareMessageContent(
service: any,
status: string,
responseTime?: number,
defaultMessage?: string
): Promise<string> {
// Use provided message if available
if (defaultMessage) {
return defaultMessage;
}
// Check for a template
let templateId = service.alertTemplate;
if (templateId && templateId !== "default") {
try {
console.log("Using specified template ID:", templateId);
const template = await pb.collection('notification_templates').getOne(templateId);
console.log("Template data:", template);
return processTemplate(
template as unknown as NotificationTemplate,
service,
status,
responseTime
);
} catch (error) {
console.error("Error fetching template:", error);
// Fall back to default message format
}
}
console.log("Using default message template");
return generateDefaultMessage(service.name, status, responseTime);
},
/**
* Send a test notification for a specific service status change
*/
async testServiceStatusNotification(serviceName: string, status: "up" | "down", responseTime?: number): Promise<boolean> {
const emoji = status === "up" ? "🟢" : "🔴";
const rtText = responseTime ? ` (Response time: ${responseTime}ms)` : "";
const message = `${emoji} Test notification: Service ${serviceName} is ${status.toUpperCase()}${rtText}`;
console.log("Test notification would be sent:", message);
// Instead of calling testTelegramNotification which no longer exists, just log and return success
return true;
},
/**
* Reset notification count for a service
* This is called when a service recovers from DOWN to UP
*/
resetNotificationCount(serviceId: string): void {
if (lastNotifications[serviceId]) {
console.log(`Resetting notification count for service ${serviceId}`);
delete lastNotifications[serviceId];
}
}
};
// Re-export the types for easier imports
export * from "./types";
// Export types from template services
export type { AnyTemplate } from '../templateService';
@@ -1,43 +1,39 @@
import { NotificationTemplate } from "../templateService";
import { AnyTemplate } from "../templateService";
/**
* Process a notification template with service data
*/
export function processTemplate(
template: NotificationTemplate,
template: AnyTemplate,
service: any,
status: string,
responseTime?: number
): string {
try {
console.log(`Processing template for status: ${status}`);
let templateText = "";
// Select the appropriate message template based on status
if (status === "up") {
templateText = template.up_message || `Service ${service.name} is now UP`;
templateText = (template as any).up_message || generateDefaultUptimeMessage(service, status, responseTime);
} else if (status === "down") {
templateText = template.down_message || `Service ${service.name} is DOWN`;
templateText = (template as any).down_message || generateDefaultUptimeMessage(service, status, responseTime);
} else if (status === "warning") {
templateText = template.incident_message || `Warning: Service ${service.name} has an incident`;
templateText = (template as any).incident_message || generateDefaultUptimeMessage(service, status, responseTime);
} else if (status === "maintenance" || status === "paused") {
templateText = template.maintenance_message || `Service ${service.name} is in maintenance mode`;
templateText = (template as any).maintenance_message || generateDefaultUptimeMessage(service, status, responseTime);
} else if (status === "resolved") {
templateText = template.resolved_message || `Issue with service ${service.name} has been resolved`;
templateText = (template as any).resolved_message || generateDefaultUptimeMessage(service, status, responseTime);
} else {
templateText = `Service ${service.name} status changed to: ${status}`;
templateText = generateDefaultUptimeMessage(service, status, responseTime);
}
// Skip replacement if template is empty
if (!templateText) {
console.log("Empty template for status:", status);
return generateDefaultMessage(service.name, status, responseTime);
return generateDefaultUptimeMessage(service, status, responseTime);
}
console.log("Using template text:", templateText);
// Replace placeholders with actual values
let message = templateText
.replace(/\${service_name}/g, service.name || 'Unknown Service')
@@ -50,22 +46,113 @@ export function processTemplate(
message = message.replace(/\${response_time}/g, 'N/A');
}
// Replace any other placeholders
// Replace service-specific placeholders
message = message
.replace(/\${threshold}/g, service.threshold || 'N/A')
.replace(/\${url}/g, service.url || 'N/A')
.replace(/\${url}/g, service.url || service.URL || 'N/A')
.replace(/\${host}/g, service.host || service.Host || 'N/A')
.replace(/\${service_type}/g, service.service_type?.toUpperCase() || service.ServiceType?.toUpperCase() || service.type?.toUpperCase() || 'N/A')
.replace(/\${port}/g, service.port ? service.port.toString() : (service.Port ? service.Port.toString() : 'N/A'))
.replace(/\${domain}/g, service.domain || service.Domain || 'N/A')
.replace(/\${region_name}/g, service.region_name || service.RegionName || 'Default')
.replace(/\${agent_id}/g, service.agent_id ? service.agent_id.toString() : (service.AgentID ? service.AgentID.toString() : '1'))
.replace(/\${uptime}/g, service.uptime ? `${service.uptime}%` : (service.Uptime ? `${service.Uptime}%` : 'N/A'))
.replace(/\${error_message}/g, service.error_message || service.ErrorMessage || service.error || '')
.replace(/\${time}/g, new Date().toLocaleString());
console.log("Processed template message:", message);
return message;
} catch (error) {
console.error("Error processing template:", error);
return generateDefaultMessage(service.name, status, responseTime);
return generateDefaultUptimeMessage(service, status, responseTime);
}
}
/**
* Generate a default message when no template is available
* Generate a default uptime message with proper formatting and emojis
*/
export function generateDefaultUptimeMessage(
service: any,
status: string,
responseTime?: number
): string {
const serviceName = service.name || service.Name || 'Unknown Service';
const statusUpper = status.toUpperCase();
// Status emoji mapping
let statusEmoji = "🔵";
if (status === "up") {
statusEmoji = "🟢";
} else if (status === "down") {
statusEmoji = "🔴";
} else if (status === "warning") {
statusEmoji = "🟡";
} else if (status === "maintenance" || status === "paused") {
statusEmoji = "🟠";
}
let message = `${statusEmoji}Service ${serviceName} is ${statusUpper}.`;
// Add service details
const host = service.host || service.Host;
const url = service.url || service.URL;
const serviceType = service.service_type || service.ServiceType || service.type;
const port = service.port || service.Port;
const domain = service.domain || service.Domain;
const regionName = service.region_name || service.RegionName;
const agentId = service.agent_id || service.AgentID;
const uptime = service.uptime || service.Uptime;
// Build formatted details
const details = [];
if (url && url !== 'N/A') {
details.push(` - Host URL: ${url}`);
} else if (host && host !== 'N/A') {
details.push(` - Host: ${host}`);
}
if (serviceType && serviceType !== 'N/A') {
details.push(` - Type: ${serviceType.toUpperCase()}`);
}
if (port && port !== 'N/A') {
details.push(` - Port: ${port}`);
}
if (domain && domain !== 'N/A') {
details.push(` - Domain: ${domain}`);
}
// Response time handling
if (responseTime !== undefined && responseTime > 0) {
details.push(` - Response time: ${responseTime}ms`);
} else {
details.push(` - Response time: N/A`);
}
if (regionName && regionName !== 'N/A') {
details.push(` - Region: ${regionName}`);
}
if (agentId && agentId !== 'N/A') {
details.push(` - Agent: ${agentId}`);
}
if (uptime !== undefined && uptime !== 'N/A') {
details.push(` - Uptime: ${uptime}%`);
}
// Add timestamp
details.push(` - Time: ${new Date().toLocaleString()}`);
// Combine message with details
if (details.length > 0) {
message += '\n' + details.join('\n');
}
return message;
}
/**
* Generate a default message when no template is available (legacy support)
*/
export function generateDefaultMessage(
serviceName: string,
@@ -83,4 +170,4 @@ export function generateDefaultMessage(
message += `. Time: ${new Date().toLocaleString()}`;
return message;
}
}
@@ -1,194 +0,0 @@
import { Service } from "@/types/service.types";
import { processTemplate, generateDefaultMessage } from "./notification/templateProcessor";
import { sendTelegramNotification, testSendTelegramMessage } from "./notification/telegramService";
import { pb } from "@/lib/pocketbase";
import { templateService } from "./templateService";
import { AlertConfiguration } from "./alertConfigService";
interface NotificationData {
service: Service;
status: string;
responseTime?: number;
timestamp: string;
_notificationSource?: string;
}
// Track last notification times for services to implement cooldown
const lastNotifications: Record<string, {
timestamp: Date;
count: number;
}> = {};
// Cooldown period in milliseconds (5 minutes)
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
export const notificationService = {
/**
* Send a notification for a service status change
*/
async sendNotification(data: NotificationData): Promise<boolean> {
try {
const { service, status, responseTime } = data;
// console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
// console.log(`Service alerts status: ${service.alerts}`);
// First check if alerts are muted for this service
// STRICT equality check against "muted" string value
if (service.alerts === "muted") {
// console.log(`NOTIFICATION BLOCKED: Alerts are muted for service: ${service.name}`);
return true; // Return true as this is expected behavior
}
// For paused status, check if this is a duplicate notification from another source
// This helps prevent the double-notification issue
if (status === "paused" && data._notificationSource === "duplicate_check") {
// console.log("NOTIFICATION BLOCKED: Duplicate pause notification detected");
return true; // Return true as this is expected behavior
}
// For DOWN status, implement the cooldown and max retries logic
if (status === "down") {
const serviceId = service.id;
const maxRetries = service.retries || 3; // Default to 3 if not specified
const now = new Date();
if (lastNotifications[serviceId]) {
const lastNotif = lastNotifications[serviceId];
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
// Check if we're within the cooldown period
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
// Increment count only if we haven't reached max retries
if (lastNotif.count < maxRetries) {
// console.log(`DOWN notification for ${service.name}: ${lastNotif.count + 1}/${maxRetries}`);
lastNotifications[serviceId].count += 1;
} else {
// console.log(`DOWN notification for ${service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
return true; // Skip notification but return success
}
} else {
// Reset count after cooldown period
// console.log(`Cooldown period elapsed for ${service.name}. Resetting notification count.`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
} else {
// First notification for this service
// console.log(`First DOWN notification for ${service.name}: 1/${maxRetries}`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
// Update timestamp for the current notification
lastNotifications[serviceId].timestamp = now;
}
// Check if notification channel is set
if (!service.notificationChannel) {
// console.log(`No notification channel set for service: ${service.name}`);
return false;
}
// Fetch the notification configuration
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
if (!alertConfigRecord) {
// console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
return false;
}
if (!alertConfigRecord.enabled) {
// console.log(`Alert configuration is disabled for service: ${service.name}`);
return false;
}
// Convert PocketBase record to AlertConfiguration
const alertConfig: AlertConfiguration = {
id: alertConfigRecord.id,
collectionId: alertConfigRecord.collectionId,
collectionName: alertConfigRecord.collectionName,
service_id: alertConfigRecord.service_id || "",
notification_type: alertConfigRecord.notification_type,
telegram_chat_id: alertConfigRecord.telegram_chat_id,
discord_webhook_url: alertConfigRecord.discord_webhook_url,
signal_number: alertConfigRecord.signal_number,
notify_name: alertConfigRecord.notify_name,
bot_token: alertConfigRecord.bot_token,
template_id: alertConfigRecord.template_id,
slack_webhook_url: alertConfigRecord.slack_webhook_url,
enabled: alertConfigRecord.enabled,
created: alertConfigRecord.created,
updated: alertConfigRecord.updated
};
// Select template if one is specified
let template;
if (service.alertTemplate) {
try {
template = await templateService.getTemplate(service.alertTemplate);
} catch (error) {
// console.error(`Error fetching template for ID: ${service.alertTemplate}`, error);
}
}
// Generate the notification message
let message;
if (template) {
message = processTemplate(template, service, status, responseTime);
} else {
message = generateDefaultMessage(service.name, status, responseTime);
}
// For DOWN status, add retry information to the message
if (status === "down" && lastNotifications[service.id]) {
const retryInfo = lastNotifications[service.id];
const maxRetries = service.retries || 3;
message += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
}
// console.log(`Prepared notification message: ${message}`);
// Send notification based on notification type
const notificationType = alertConfig.notification_type;
if (notificationType === 'telegram') {
return await sendTelegramNotification(alertConfig, message);
}
// For other types like discord, slack, etc. (not implemented yet)
// console.log(`Notification type ${notificationType} not implemented yet`);
return false;
} catch (error) {
// console.error("Error sending notification:", error);
return false;
}
},
/**
* Send a test notification for a service status change
*/
async testServiceStatusNotification(serviceName: string, status: string, responseTime?: number): Promise<boolean> {
try {
const message = status === 'up'
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
: `Service ${serviceName} is DOWN`;
// console.log(`Test notification would have been sent: ${message}`);
return true; // Just log, don't actually send
} catch (error) {
// console.error("Error in test notification:", error);
return false;
}
},
/**
* Reset notification count for a service
* This can be called when a service recovers from DOWN to UP
*/
resetNotificationCount(serviceId: string): void {
if (lastNotifications[serviceId]) {
// console.log(`Resetting notification count for service ${serviceId}`);
delete lastNotifications[serviceId];
}
}
};
@@ -27,7 +27,7 @@ export const operationalPageService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching operational pages:', error);
// console.error('Error fetching operational pages:', error);
throw error;
}
},
@@ -55,7 +55,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error fetching operational page:', error);
// console.error('Error fetching operational page:', error);
throw error;
}
},
@@ -84,7 +84,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error updating operational page:', error);
// console.error('Error updating operational page:', error);
throw error;
}
},
@@ -113,7 +113,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error creating operational page:', error);
// console.error('Error creating operational page:', error);
throw error;
}
},
@@ -139,7 +139,7 @@ export const operationalPageService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Error deleting operational page:', error);
// console.error('Error deleting operational page:', error);
throw error;
}
}
@@ -0,0 +1,114 @@
import { pb } from "@/lib/pocketbase";
export interface ServerNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
ram_message: string;
cpu_message: string;
disk_message: string;
network_message: string;
up_message: string;
down_message: string;
notification_id: string;
warning_message: string;
paused_message: string;
cpu_temp_message: string;
disk_io_message: string;
restore_ram_message: string;
restore_cpu_message: string;
restore_disk_message: string;
restore_network_message: string;
restore_disk_io_message: string;
restore_cpu_temp_message: string;
placeholder: string;
created: string;
updated: string;
}
export interface CreateUpdateServerNotificationTemplateData {
name: string;
ram_message: string;
cpu_message: string;
disk_message: string;
network_message: string;
up_message: string;
down_message: string;
notification_id: string;
warning_message: string;
paused_message: string;
cpu_temp_message: string;
disk_io_message: string;
restore_ram_message: string;
restore_cpu_message: string;
restore_disk_message: string;
restore_network_message: string;
restore_disk_io_message: string;
restore_cpu_temp_message: string;
placeholder: string;
}
export const serverNotificationTemplateService = {
async getTemplates(): Promise<ServerNotificationTemplate[]> {
try {
const response = await pb.collection('server_notification_templates').getList(1, 50, {
sort: '-created',
});
return response.items as unknown as ServerNotificationTemplate[];
} catch (error) {
throw error;
}
},
async getTemplate(id: string): Promise<ServerNotificationTemplate> {
try {
const response = await pb.collection('server_notification_templates').getOne(id);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
throw error;
}
},
async createTemplate(data: CreateUpdateServerNotificationTemplateData): Promise<ServerNotificationTemplate> {
try {
const response = await pb.collection('server_notification_templates').create(data);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateServerNotificationTemplateData>): Promise<ServerNotificationTemplate> {
try {
const response = await pb.collection('server_notification_templates').update(id, data);
return response as unknown as ServerNotificationTemplate;
} catch (error) {
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
await pb.collection('server_notification_templates').delete(id);
return true;
} catch (error) {
throw error;
}
}
};
@@ -0,0 +1,93 @@
import { pb } from "@/lib/pocketbase";
export interface ServiceNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
up_message: string;
down_message: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
placeholder: string;
warning_message: string;
created: string;
updated: string;
}
export interface CreateUpdateServiceNotificationTemplateData {
name: string;
up_message: string;
down_message: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
placeholder: string;
warning_message: string;
}
export const serviceNotificationTemplateService = {
async getTemplates(): Promise<ServiceNotificationTemplate[]> {
try {
// console.log("Fetching service notification templates");
const response = await pb.collection('service_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("Service notification templates response:", response);
return response.items as unknown as ServiceNotificationTemplate[];
} catch (error) {
// console.error("Error fetching service notification templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise<ServiceNotificationTemplate> {
try {
// console.log(`Fetching service notification template with id: ${id}`);
const response = await pb.collection('service_notification_templates').getOne(id);
// console.log("Service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error(`Error fetching service notification template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateServiceNotificationTemplateData): Promise<ServiceNotificationTemplate> {
try {
// console.log("Creating new service notification template with data:", data);
const response = await pb.collection('service_notification_templates').create(data);
// console.log("Create service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error("Error creating service notification template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateServiceNotificationTemplateData>): Promise<ServiceNotificationTemplate> {
try {
// console.log(`Updating service notification template with id: ${id}`, data);
const response = await pb.collection('service_notification_templates').update(id, data);
// console.log("Update service notification template response:", response);
return response as unknown as ServiceNotificationTemplate;
} catch (error) {
// console.error(`Error updating service notification template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting service notification template with id: ${id}`);
await pb.collection('service_notification_templates').delete(id);
// console.log("Service notification template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting service notification template with id ${id}:`, error);
throw error;
}
}
};
+17 -27
View File
@@ -9,7 +9,6 @@ export type { Service, UptimeData, CreateServiceParams };
export const serviceService = {
async getServices(): Promise<Service[]> {
try {
// First get the total count of records
const countResponse = await pb.collection('services').getList(1, 1, {
sort: 'name',
@@ -37,7 +36,7 @@ export const serviceService = {
retries: item.max_retries || item.retries || 3,
notificationChannel: item.notification_id,
notification_channel: item.notification_channel, // Add this field for multiple channels support
notification_status: item.notification_status || "disabled",
notification_status: item.notification_status || false,
alertTemplate: item.template_id,
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
alerts: item.alerts || "unmuted", // Store actual database field
@@ -49,7 +48,6 @@ export const serviceService = {
regional_monitoring_enabled: item.regional_status === "enabled", // Backward compatibility
}));
} catch (error) {
// console.error("Error fetching services:", error);
throw new Error('Failed to load services data.');
}
},
@@ -60,7 +58,6 @@ export const serviceService = {
const serviceType = params.type.toLowerCase();
// Debug log to check what we're sending
// console.log("Creating service with params:", params);
const data = {
name: params.name,
@@ -71,16 +68,16 @@ export const serviceService = {
last_checked: new Date().toLocaleString(),
heartbeat_interval: params.interval,
max_retries: params.retries,
notification_status: params.notificationStatus || "disabled",
// Store multiple notification channels as JSON string
// Store notification_status as boolean
notification_status: params.notificationStatus === true,
// Always store channels and template if provided (don't clear based on status)
notification_channel: params.notificationChannels && params.notificationChannels.length > 0
? JSON.stringify(params.notificationChannels)
: null,
// Store multiple notification IDs as comma-separated string in notification_id field
notification_id: params.notificationChannels && params.notificationChannels.length > 0
? params.notificationChannels.join(',')
: null,
template_id: params.alertTemplate,
template_id: params.alertTemplate || null,
// Regional monitoring fields - use regional_status
regional_status: params.regionalStatus || "disabled",
region_name: params.regionName || "",
@@ -96,9 +93,7 @@ export const serviceService = {
)
};
// console.log("Creating service with data:", data);
const record = await pb.collection('services').create(data);
// console.log("Service created, returned record:", record);
// Return the newly created service
const newService = {
@@ -117,7 +112,7 @@ export const serviceService = {
retries: record.max_retries || 3,
notificationChannel: record.notification_id,
notification_channel: record.notification_channel,
notification_status: record.notification_status || "disabled",
notification_status: record.notification_status || false,
alertTemplate: record.template_id,
regional_status: record.regional_status || "disabled",
regional_monitoring_enabled: record.regional_status === "enabled",
@@ -130,7 +125,6 @@ export const serviceService = {
return newService;
} catch (error) {
// console.error("Error creating service:", error);
throw new Error('Failed to create service.');
}
},
@@ -141,23 +135,22 @@ export const serviceService = {
const serviceType = params.type.toLowerCase();
// Debug log to check what we're updating
// console.log("Updating service with params:", params);
const data = {
name: params.name,
service_type: serviceType,
heartbeat_interval: params.interval,
max_retries: params.retries,
notification_status: params.notificationStatus || "disabled",
// Store multiple notification channels as JSON string
notification_channel: params.notificationChannels && params.notificationChannels.length > 0
? JSON.stringify(params.notificationChannels)
: null,
// Store multiple notification IDs as comma-separated string in notification_id field
notification_id: params.notificationChannels && params.notificationChannels.length > 0
? params.notificationChannels.join(',')
: null,
template_id: params.alertTemplate || null,
// Store notification_status as boolean - preserve existing channels/template
notification_status: params.notificationStatus === true,
// Only update channels and template if they are provided (don't clear them)
...(params.notificationChannels && params.notificationChannels.length > 0 && {
notification_channel: JSON.stringify(params.notificationChannels),
notification_id: params.notificationChannels.join(',')
}),
...(params.alertTemplate && {
template_id: params.alertTemplate
}),
// Regional monitoring fields - use regional_status
regional_status: params.regionalStatus || "disabled",
region_name: params.regionName || "",
@@ -173,7 +166,6 @@ export const serviceService = {
)
};
// console.log("Updating service with data:", data);
// Use timeout to ensure the request doesn't hang
const timeoutPromise = new Promise<never>((_, reject) => {
@@ -182,7 +174,6 @@ export const serviceService = {
const updatePromise = pb.collection('services').update(id, data);
const record = await Promise.race([updatePromise, timeoutPromise]) as any;
// console.log("Service updated, returned record:", record);
// Return the updated service
const updatedService = {
@@ -201,7 +192,7 @@ export const serviceService = {
retries: record.max_retries || 3,
notificationChannel: record.notification_id,
notification_channel: record.notification_channel,
notification_status: record.notification_status || "disabled",
notification_status: record.notification_status || false,
alertTemplate: record.template_id,
regional_status: record.regional_status || "disabled",
regional_monitoring_enabled: record.regional_status === "enabled",
@@ -211,7 +202,6 @@ export const serviceService = {
return updatedService;
} catch (error) {
//console.error("Error updating service:", error);
throw new Error(`Failed to update service: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
@@ -13,6 +13,8 @@ export const addSSLCertificate = async (
certificateData: AddSSLCertificateDto
): Promise<SSLCertificate> => {
try {
const currentTime = new Date().toISOString();
// Prepare the data for saving to database
// The Go service will handle the actual SSL checking
const data = {
@@ -23,14 +25,17 @@ export const addSSLCertificate = async (
cert_sans: "",
cert_alg: "",
serial_number: "",
valid_from: new Date().toISOString(), // Will be updated by Go service
valid_till: new Date().toISOString(), // Will be updated by Go service
valid_from: currentTime, // Will be updated by Go service
valid_till: currentTime, // Will be updated by Go service
validity_days: 0, // Will be updated by Go service
days_left: 0, // Will be updated by Go service
warning_threshold: Number(certificateData.warning_threshold) || 30,
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
notification_channel: certificateData.notification_channel || "",
notification_id: certificateData.notification_id || "", // Multi notification channels
template_id: certificateData.template_id || "", // Alert template ID
check_interval: Number(certificateData.check_interval) || 1, // New field
check_at: currentTime, // Set to current time to trigger immediate check
};
// Save to database
@@ -38,7 +43,6 @@ export const addSSLCertificate = async (
return record as unknown as SSLCertificate;
} catch (error) {
console.error("Error adding SSL certificate:", error);
throw error;
}
};
@@ -67,7 +71,6 @@ export const checkAndUpdateCertificate = async (
// Return the current certificate data
return typedCertificate;
} catch (error) {
console.error("Error updating SSL certificate:", error);
throw error;
}
};
@@ -84,10 +87,8 @@ export const triggerImmediateCheck = async (certificateId: string): Promise<void
check_at: currentTime
});
console.log(`Triggered immediate check for certificate ${certificateId} at ${currentTime}`);
toast.success("SSL check scheduled - certificate will be checked shortly");
} catch (error) {
console.error("Error triggering immediate SSL check:", error);
toast.error("Failed to schedule SSL check");
throw error;
}
@@ -101,7 +102,6 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
await pb.collection("ssl_certificates").delete(id);
return true;
} catch (error) {
console.error("Error deleting SSL certificate:", error);
throw error;
}
};
@@ -112,11 +112,9 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
*/
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
try {
const response = await pb.collection("ssl_certificates").getList(1, 200);
const response = await pb.collection("ssl_certificates").getList(1, 100);
const certificates = response.items as unknown as SSLCertificate[];
// console.log(`Refreshing ${certificates.length} certificates...`);
let success = 0;
let failed = 0;
@@ -125,14 +123,12 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
await checkCertificateAndNotify(cert);
success++;
} catch (error) {
// console.error(`Failed to refresh certificate ${cert.domain}:`, error);
failed++;
}
}
return { success, failed };
} catch (error) {
// console.error("Error refreshing certificates:", error);
throw error;
}
};
+8
View File
@@ -1,9 +1,13 @@
// SSL Certificate DTO for adding new certificates
export interface AddSSLCertificateDto {
domain: string;
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
alert_template?: string; // New field for SSL alert template
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
check_interval?: number; // New field for check interval in days
}
@@ -25,6 +29,10 @@ export interface SSLCertificate {
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
alert_template?: string; // New field for SSL alert template
// PocketBase specific fields
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
last_notified?: string;
created?: string;
updated?: string;
@@ -0,0 +1,87 @@
import { pb } from "@/lib/pocketbase";
export interface SslNotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
expired: string;
exiring_soon: string;
warning: string;
placeholder: string;
created: string;
updated: string;
}
export interface CreateUpdateSslNotificationTemplateData {
name: string;
expired: string;
exiring_soon: string;
warning: string;
placeholder: string;
}
export const sslNotificationTemplateService = {
async getTemplates(): Promise<SslNotificationTemplate[]> {
try {
// console.log("Fetching SSL notification templates");
const response = await pb.collection('ssl_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("SSL notification templates response:", response);
return response.items as unknown as SslNotificationTemplate[];
} catch (error) {
// console.error("Error fetching SSL notification templates:", error);
throw error;
}
},
async getTemplate(id: string): Promise<SslNotificationTemplate> {
try {
// console.log(`Fetching SSL notification template with id: ${id}`);
const response = await pb.collection('ssl_notification_templates').getOne(id);
// console.log("SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error(`Error fetching SSL notification template with id ${id}:`, error);
throw error;
}
},
async createTemplate(data: CreateUpdateSslNotificationTemplateData): Promise<SslNotificationTemplate> {
try {
// console.log("Creating new SSL notification template with data:", data);
const response = await pb.collection('ssl_notification_templates').create(data);
// console.log("Create SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error("Error creating SSL notification template:", error);
throw error;
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateSslNotificationTemplateData>): Promise<SslNotificationTemplate> {
try {
// console.log(`Updating SSL notification template with id: ${id}`, data);
const response = await pb.collection('ssl_notification_templates').update(id, data);
// console.log("Update SSL notification template response:", response);
return response as unknown as SslNotificationTemplate;
} catch (error) {
// console.error(`Error updating SSL notification template with id ${id}:`, error);
throw error;
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting SSL notification template with id: ${id}`);
await pb.collection('ssl_notification_templates').delete(id);
// console.log("SSL notification template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting SSL notification template with id ${id}:`, error);
throw error;
}
}
};
@@ -27,7 +27,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching status page components:', error);
// console.error('Error fetching status page components:', error);
throw error;
}
},
@@ -56,7 +56,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching status page components by operational ID:', error);
// console.error('Error fetching status page components by operational ID:', error);
throw error;
}
},
@@ -79,7 +79,7 @@ export const statusPageComponentsService = {
headers['Authorization'] = `Bearer ${authToken}`;
}
console.log('Creating status page component with data:', data);
// console.log('Creating status page component with data:', data);
const baseUrl = getCurrentEndpoint();
const response = await fetch(`${baseUrl}/api/collections/status_page_components/records`, {
@@ -95,10 +95,10 @@ export const statusPageComponentsService = {
}
const result = await response.json();
console.log('Created component:', result);
// console.log('Created component:', result);
return result;
} catch (error) {
console.error('Error creating status page component:', error);
// console.error('Error creating status page component:', error);
throw error;
}
},
@@ -124,7 +124,7 @@ export const statusPageComponentsService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Error deleting status page component:', error);
// console.error('Error deleting status page component:', error);
throw error;
}
}
+123 -80
View File
@@ -1,100 +1,143 @@
import { pb } from "@/lib/pocketbase";
import {
serverNotificationTemplateService,
ServerNotificationTemplate,
CreateUpdateServerNotificationTemplateData
} from "./serverNotificationTemplateService";
import {
serviceNotificationTemplateService,
ServiceNotificationTemplate,
CreateUpdateServiceNotificationTemplateData
} from "./serviceNotificationTemplateService";
import {
sslNotificationTemplateService,
SslNotificationTemplate,
CreateUpdateSslNotificationTemplateData
} from "./sslNotificationTemplateService";
import {
serverThresholdService,
ServerThreshold,
CreateUpdateServerThresholdData
} from "./serverThresholdService";
export interface NotificationTemplate {
id: string;
collectionId: string;
collectionName: string;
name: string;
up_message: string;
down_message: string;
service_name_placeholder: string;
response_time_placeholder: string;
status_placeholder: string;
threshold_placeholder: string;
type: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
created: string;
updated: string;
}
export type TemplateType = 'server' | 'service' | 'ssl' | 'server_threshold';
export interface CreateUpdateTemplateData {
name: string;
up_message: string;
down_message: string;
service_name_placeholder: string;
response_time_placeholder: string;
status_placeholder: string;
threshold_placeholder: string;
type: string;
maintenance_message: string;
incident_message: string;
resolved_message: string;
}
export type AnyTemplate = ServerNotificationTemplate | ServiceNotificationTemplate | SslNotificationTemplate | ServerThreshold;
export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData | CreateUpdateServerThresholdData;
// Export individual template types
export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate, ServerThreshold };
export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData, CreateUpdateServerThresholdData };
export const templateService = {
async getTemplates(): Promise<NotificationTemplate[]> {
try {
// console.log("Fetching server notification templates");
const response = await pb.collection('server_notification_templates').getList(1, 50, {
sort: '-created',
});
// console.log("Server templates response:", response);
return response.items as unknown as NotificationTemplate[];
} catch (error) {
// console.error("Error fetching server templates:", error);
throw error;
async getTemplates(type: TemplateType): Promise<AnyTemplate[]> {
switch (type) {
case 'server':
return serverNotificationTemplateService.getTemplates();
case 'service':
return serviceNotificationTemplateService.getTemplates();
case 'ssl':
return sslNotificationTemplateService.getTemplates();
case 'server_threshold':
return serverThresholdService.getServerThresholds();
default:
throw new Error(`Unknown template type: ${type}`);
}
},
async getTemplate(id: string): Promise<NotificationTemplate> {
try {
// console.log(`Fetching server template with id: ${id}`);
const response = await pb.collection('server_notification_templates').getOne(id);
// console.log("Server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
// console.error(`Error fetching server template with id ${id}:`, error);
throw error;
async getTemplate(id: string, type: TemplateType): Promise<AnyTemplate> {
switch (type) {
case 'server':
return serverNotificationTemplateService.getTemplate(id);
case 'service':
return serviceNotificationTemplateService.getTemplate(id);
case 'ssl':
return sslNotificationTemplateService.getTemplate(id);
case 'server_threshold':
return serverThresholdService.getServerThreshold(id);
default:
throw new Error(`Unknown template type: ${type}`);
}
},
async createTemplate(data: CreateUpdateTemplateData): Promise<NotificationTemplate> {
try {
// console.log("Creating new server template with data:", data);
const response = await pb.collection('server_notification_templates').create(data);
// console.log("Create server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
// console.error("Error creating server template:", error);
throw error;
async createTemplate(data: AnyTemplateData, type: TemplateType): Promise<AnyTemplate> {
switch (type) {
case 'server':
return serverNotificationTemplateService.createTemplate(data as CreateUpdateServerNotificationTemplateData);
case 'service':
return serviceNotificationTemplateService.createTemplate(data as CreateUpdateServiceNotificationTemplateData);
case 'ssl':
return sslNotificationTemplateService.createTemplate(data as CreateUpdateSslNotificationTemplateData);
case 'server_threshold':
return serverThresholdService.createServerThreshold(data as CreateUpdateServerThresholdData);
default:
throw new Error(`Unknown template type: ${type}`);
}
},
async updateTemplate(id: string, data: Partial<CreateUpdateTemplateData>): Promise<NotificationTemplate> {
try {
// console.log(`Updating server template with id: ${id}`, data);
const response = await pb.collection('server_notification_templates').update(id, data);
// console.log("Update server template response:", response);
return response as unknown as NotificationTemplate;
} catch (error) {
// console.error(`Error updating server template with id ${id}:`, error);
throw error;
async updateTemplate(id: string, data: Partial<AnyTemplateData>, type: TemplateType): Promise<AnyTemplate> {
switch (type) {
case 'server':
return serverNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServerNotificationTemplateData>);
case 'service':
return serviceNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServiceNotificationTemplateData>);
case 'ssl':
return sslNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateSslNotificationTemplateData>);
case 'server_threshold':
return serverThresholdService.updateServerThreshold(id, data as Partial<CreateUpdateServerThresholdData>);
default:
throw new Error(`Unknown template type: ${type}`);
}
},
async deleteTemplate(id: string): Promise<boolean> {
try {
// console.log(`Deleting server template with id: ${id}`);
await pb.collection('server_notification_templates').delete(id);
// console.log("Server template deleted successfully");
return true;
} catch (error) {
// console.error(`Error deleting server template with id ${id}:`, error);
throw error;
async deleteTemplate(id: string, type: TemplateType): Promise<boolean> {
switch (type) {
case 'server':
return serverNotificationTemplateService.deleteTemplate(id);
case 'service':
return serviceNotificationTemplateService.deleteTemplate(id);
case 'ssl':
return sslNotificationTemplateService.deleteTemplate(id);
case 'server_threshold':
return serverThresholdService.deleteServerThreshold(id);
default:
throw new Error(`Unknown template type: ${type}`);
}
}
};
// Template type configurations
export const templateTypeConfigs = {
server: {
label: 'Server Monitoring',
description: 'Templates for server resource monitoring alerts',
placeholders: [
'${server_name}', '${cpu_usage}', '${ram_usage}', '${disk_usage}',
'${network_usage}', '${cpu_temp}', '${disk_io}', '${threshold}', '${time}'
]
},
service: {
label: 'Service Uptime',
description: 'Templates for service uptime monitoring alerts',
placeholders: [
'${service_name}', '${status}', '${response_time}', '${url}',
'${host}', '${service_type}', '${port}', '${domain}',
'${region_name}', '${agent_id}', '${uptime}', '${time}'
]
},
ssl: {
label: 'SSL Certificate',
description: 'Templates for SSL certificate monitoring alerts',
placeholders: [
'${domain}', '${certificate_name}', '${expiry_date}', '${days_left}',
'${issuer}', '${serial_number}', '${time}'
]
},
server_threshold: {
label: 'Server Threshold',
description: 'Templates for server resource threshold configurations',
placeholders: [
'${name}', '${cpu_threshold}', '${ram_threshold}', '${disk_threshold}', '${network_threshold}'
]
}
};
@@ -0,0 +1,66 @@
import { pb } from "@/lib/pocketbase";
import { toast } from "@/hooks/use-toast";
export interface WebhookConfiguration {
id?: string;
collectionId?: string;
collectionName?: string;
user_id?: string;
url: string;
enabled: string;
secret?: string;
headers?: string;
retry_count?: string;
trigger_events?: string;
description?: string;
name: string;
method?: string;
payload_template?: string;
created?: string;
updated?: string;
}
export const webhookService = {
async createWebhook(config: Omit<WebhookConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<WebhookConfiguration | null> {
// console.info("Creating webhook configuration:", config);
try {
const result = await pb.collection('webhook').create(config);
// console.info("Webhook configuration created:", result);
toast({
title: "Success",
description: "Webhook created successfully",
});
return result as WebhookConfiguration;
} catch (error) {
// console.error("Error creating webhook configuration:", error);
toast({
title: "Error",
description: "Failed to create webhook",
variant: "destructive"
});
return null;
}
},
async updateWebhook(id: string, config: Partial<WebhookConfiguration>): Promise<WebhookConfiguration | null> {
// console.info(`Updating webhook configuration ${id}:`, config);
try {
const result = await pb.collection('webhook').update(id, config);
// console.info("Webhook configuration updated:", result);
toast({
title: "Success",
description: "Webhook updated successfully",
});
return result as WebhookConfiguration;
} catch (error) {
// console.error("Error updating webhook configuration:", error);
toast({
title: "Error",
description: "Failed to update webhook",
variant: "destructive"
});
return null;
}
}
};
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
english: "Englisch",
khmer: "Khmer",
german: "Deutsch",
simplifiedChinese: "Simplified Chinese",
goodMorning: "Guten Morgen",
goodAfternoon: "Guten Nachmittag",
goodEvening: "Guten Abend",
+9
View File
@@ -10,6 +10,7 @@ export const sslTranslations: SSLTranslations = {
deleteSSLCertificate: "Delete SSL Certificate",
sslCertificateDetails: "SSL-Zertifikatdetails",
detailedInfo: "Detaillierte Informationen zum SSL-Zertifikat für",
viewDetailedInformation: "View detailed information for",
// Status related
valid: "Gültig",
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
validFrom: "Gültig von",
validUntil: "Gültig bis",
validityDays: "Gültigkeitsdauer Tage",
validityPeriod: "Validity Period",
organization: "Organisation",
commonName: "Allgemeiner Name",
serialNumber: "Seriennummer",
algorithm: "Algorithmus",
subjectAltNames: "Thema Alternative Bezeichnungen",
subjectAlternativeNames: "Subject Alternative Names",
resolvedIP: "Resolved IP",
issuedTo: "Issued To",
days: "days",
// Buttons and actions
addDomain: "Domain hinzufügen",
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
validity: "Gültigkeit",
issuerInfo: "Informationen zum Aussteller",
technicalDetails: "Technische Details",
technicalInformation: "Technical Information",
monitoringConfig: "Monitoring-Konfiguration",
monitoringConfiguration: "Monitoring Configuration",
recordInfo: "Aufzeichnungsinformationen",
certificateDetails: "Certificate Details",
// Notifications and messages
sslCertificateAdded: "SSL-Zertifikat erfolgreich hinzugefügt",
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
english: "English",
khmer: "Khmer",
german: "Deutsch",
simplifiedChinese: "Simplified Chinese",
goodMorning: "Good morning",
goodAfternoon: "Good afternoon",
goodEvening: "Good evening",
+9
View File
@@ -10,6 +10,7 @@ export const sslTranslations: SSLTranslations = {
deleteSSLCertificate: "Delete SSL Certificate",
sslCertificateDetails: "SSL Certificate Details",
detailedInfo: "Detailed information for",
viewDetailedInformation: "View detailed information for",
// Status related
valid: "Valid",
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
validFrom: "Valid From",
validUntil: "Valid Until",
validityDays: "Validity Days",
validityPeriod: "Validity Period",
organization: "Organization",
commonName: "Common Name",
serialNumber: "Serial Number",
algorithm: "Algorithm",
subjectAltNames: "Subject Alternative Names",
subjectAlternativeNames: "Subject Alternative Names",
resolvedIP: "Resolved IP",
issuedTo: "Issued To",
days: "days",
// Buttons and actions
addDomain: "Add Domain",
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
validity: "Validity",
issuerInfo: "Issuer Information",
technicalDetails: "Technical Details",
technicalInformation: "Technical Information",
monitoringConfig: "Monitoring Configuration",
monitoringConfiguration: "Monitoring Configuration",
recordInfo: "Record Information",
certificateDetails: "Certificate Details",
// Notifications and messages
sslCertificateAdded: "SSL Certificate added successfully",
+3 -1
View File
@@ -2,14 +2,16 @@ import enTranslations from './en';
import kmTranslations from './km';
import deTranslations from './de';
import jaTranslations from './ja';
import zhcnTranslations from './zhcn';
export type Language = "en" | "km" | "de" | "ja";
export type Language = "en" | "km" | "de" | "ja" | "zhcn";
export const translations = {
en: enTranslations,
km: kmTranslations,
de: deTranslations,
ja: jaTranslations,
zhcn: zhcnTranslations,
};
// Type for accessing translations by module and key
+2 -1
View File
@@ -7,6 +7,7 @@ export const commonTranslations: CommonTranslations = {
english: "English",
khmer: "Khmer",
german: "Deutsch",
simplifiedChinese: "简体中文",
japanese: "日本語",
goodMorning: "おはようございます",
goodAfternoon: "こんにちは",
@@ -26,4 +27,4 @@ export const commonTranslations: CommonTranslations = {
description: "説明",
success: "成功",
error: "エラー",
};
};
+9
View File
@@ -9,6 +9,7 @@ export const sslTranslations: SSLTranslations = {
deleteSSLCertificate: "SSL証明書を削除",
sslCertificateDetails: "SSL証明書詳細",
detailedInfo: "詳細情報:",
viewDetailedInformation: "View detailed information for",
// ステータス関連
valid: "有効",
@@ -47,11 +48,16 @@ export const sslTranslations: SSLTranslations = {
validFrom: "有効開始日",
validUntil: "有効終了日",
validityDays: "有効期間",
validityPeriod: "Validity Period",
organization: "組織",
commonName: "コモンネーム",
serialNumber: "シリアル番号",
algorithm: "アルゴリズム",
subjectAltNames: "サブジェクト代替名",
subjectAlternativeNames: "Subject Alternative Names",
resolvedIP: "Resolved IP",
issuedTo: "Issued To",
days: "days",
// ボタンとアクション
addDomain: "ドメインを追加",
@@ -71,8 +77,11 @@ export const sslTranslations: SSLTranslations = {
validity: "有効性",
issuerInfo: "発行者情報",
technicalDetails: "技術的詳細",
technicalInformation: "Technical Information",
monitoringConfig: "監視設定",
monitoringConfiguration: "Monitoring Configuration",
recordInfo: "レコード情報",
certificateDetails: "Certificate Details",
// 通知とメッセージ
sslCertificateAdded: "SSL証明書が正常に追加されました",
@@ -8,6 +8,7 @@ export const commonTranslations: CommonTranslations = {
english: "អង់គ្លេស",
khmer: "ខ្មែរ",
german: "Deutsch",
simplifiedChinese: "简体中文",
goodMorning: "អរុណសួស្តី",
goodAfternoon: "ទិវាសួស្តី",
goodEvening: "សាយណ្ហសួស្តី",
+10 -1
View File
@@ -9,7 +9,8 @@ export const sslTranslations: SSLTranslations = {
editSSLCertificate: "កែសម្រួលវិញ្ញាបនបត្រ SSL",
deleteSSLCertificate: "លុបវិញ្ញាបនបត្រ SSL",
sslCertificateDetails: "ព័ត៌មានលម្អិតនៃវិញ្ញាបនបត្រ SSL",
detailedInfo: "ព័ត៌មានលម្អិតសម្រាប់",
detailedInfo: "ព័ត៌មានលម្អិត",
viewDetailedInformation: "មើលព័ត៌មានលម្អិត",
// Status related
valid: "មានសុពលភាព",
@@ -48,11 +49,16 @@ export const sslTranslations: SSLTranslations = {
validFrom: "មានសុពលភាពចាប់ពី",
validUntil: "មានសុពលភាពរហូតដល់",
validityDays: "ថ្ងៃសុពលភាព",
validityPeriod: "រយៈពេលសុពលភាព",
organization: "អង្គការ",
commonName: "ឈ្មោះទូទៅ",
serialNumber: "លេខសៀរៀល",
algorithm: "អាល់ហ្គោរីត",
subjectAltNames: "ឈ្មោះផ្សេងៗដែលអាចប្រើបាន",
subjectAlternativeNames: "ឈ្មោះជំនួសផ្សេងៗ",
resolvedIP: "IP របស់ដូមែន",
issuedTo: "ចេញសម្រាប់",
days: "ថ្ងៃ",
// Buttons and actions
addDomain: "បន្ថែមដូមេន",
@@ -72,8 +78,11 @@ export const sslTranslations: SSLTranslations = {
validity: "សុពលភាព",
issuerInfo: "ព័ត៌មានអ្នកចេញផ្សាយ",
technicalDetails: "ព័ត៌មានបច្ចេកទេស",
technicalInformation: "ព័ត៌មានបច្ចេកទេស",
monitoringConfig: "ការកំណត់រចនាសម្ព័ន្ធការតាមដាន",
monitoringConfiguration: "ការកំណត់រចនាសម្ព័ន្ធការតាមដាន",
recordInfo: "ព័ត៌មានកំណត់ត្រា",
certificateDetails: "ព័ត៌មានលម្អិតវិញ្ញាបនបត្រ",
// Notifications and messages
sslCertificateAdded: "វិញ្ញាបនបត្រ SSL បានបន្ថែមដោយជោគជ័យ",
@@ -5,6 +5,9 @@ export interface CommonTranslations {
language: string;
english: string;
khmer: string;
german: string;
japanese: string;
simplifiedChinese: string;
goodMorning: string;
goodAfternoon: string;
goodEvening: string;
+10 -1
View File
@@ -8,6 +8,7 @@ export interface SSLTranslations {
deleteSSLCertificate: string;
sslCertificateDetails: string;
detailedInfo: string;
viewDetailedInformation: string;
// Status related
valid: string;
@@ -46,11 +47,16 @@ export interface SSLTranslations {
validFrom: string;
validUntil: string;
validityDays: string;
validityPeriod: string;
organization: string;
commonName: string;
serialNumber: string;
algorithm: string;
subjectAltNames: string;
subjectAlternativeNames: string;
resolvedIP: string;
issuedTo: string;
days: string;
// Buttons and actions
addDomain: string;
@@ -70,8 +76,11 @@ export interface SSLTranslations {
validity: string;
issuerInfo: string;
technicalDetails: string;
technicalInformation: string;
monitoringConfig: string;
monitoringConfiguration: string;
recordInfo: string;
certificateDetails: string;
// Notifications and messages
sslCertificateAdded: string;
@@ -102,4 +111,4 @@ export interface SSLTranslations {
lastUpdated: string;
lastNotification: string;
collectionId: string;
}
}
@@ -0,0 +1,18 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "关于 Checkcle",
systemDescription: "Checkcle 是一个开源监控平台,可提供有关服务器和服务健康状况的实时洞察、事件管理以及透明化运营。以 MIT 许可证发布。",
systemVersion: "系统版本",
license: "许可证",
mitLicense: "MIT 许可证",
links: "链接",
viewOnGithub: "在 GitHub 上查看",
viewDocumentation: "查看文档",
followOnX: "在 X 上关注",
joinDiscord: "加入 Discord",
quickActions: "快速操作",
quickActionsDescription: "快速访问常用的监控操作和功能。选择下面的操作开始。",
quickTips: "快速提示",
};
@@ -0,0 +1,30 @@
import { CommonTranslations } from '../types/common';
export const commonTranslations: CommonTranslations = {
welcome: "欢迎",
logout: "注销",
language: "语言",
english: "English",
khmer: "Khmer",
german: "Deutsch",
simplifiedChinese: "简体中文",
goodMorning: "早上好",
goodAfternoon: "下午好",
goodEvening: "晚上好",
profile: "个人资料",
settings: "设置",
documentation: "文档",
notifications: "通知",
close: "关闭",
cancel: "取消",
view: "查看",
edit: "编辑",
delete: "删除",
status: "状态",
time: "时间",
title: "标题",
description: "描述",
success: "成功",
error: "错误",
};
@@ -0,0 +1,55 @@
import { IncidentTranslations } from '../types/incident';
export const incidentTranslations: IncidentTranslations = {
incidentManagement: '事件管理',
incidentsManagementDesc: '跟踪和管理服务事件及其解决方案',
unresolvedIncidents: '未解决',
resolvedIncidents: '已解决',
activeIncidents: '活动事件',
criticalIssues: '关键问题',
avgResolutionTime: '平均解决时间',
noIncidents: '没有活动事件',
createIncident: '创建事件',
investigating: '调查中',
identified: '已识别',
monitoring: '监控',
resolved: '已解决',
scheduleIncidentManagement: '计划与事件管理',
incidentName: '事件名称',
incidentStatus: '事件状态',
highPriority: '高优先级',
configurationSettings: '配置设置',
incidentCreatedSuccess: '事件创建成功',
basicInfo: '基本信息',
serviceId: '服务 ID',
assignedTo: '分配给',
unassigned: '未分配',
timeline: '时间线',
incidentTime: '事件时间',
resolutionTime: '解决时间',
systems: '系统',
noSystems: '没有受影响的系统',
impactAnalysis: '影响分析',
rootCause: '根本原因',
resolutionSteps: '解决步骤',
lessonsLearned: '经验',
resolutionDetails: '解决详情',
assignment: '分配',
download: '下载',
downloadPdf: '下载 PDF',
print: '打印',
confidentialNote: '此文档为机密文件,仅供内部使用。',
generatedOn: '生成时间',
enterResolutionSteps: '输入解决此事件的步骤',
enterLessonsLearned: '输入从此事件中学到的经验',
editIncident: '编辑事件',
editIncidentDesc: '更新此事件的详细信息',
updating: '更新中...',
update: '更新',
create: '创建',
creating: '创建中...',
configuration: '配置',
failedToUpdateStatus: '更新状态失败',
inProgress: '进行中',
};
@@ -0,0 +1,25 @@
import { Translations } from '../types';
import { commonTranslations } from './common';
import { menuTranslations } from './menu';
import { loginTranslations } from './login';
import { aboutTranslations } from './about';
import { servicesTranslations } from './services';
import { maintenanceTranslations } from './maintenance';
import { incidentTranslations } from './incident';
import { sslTranslations } from './ssl';
import { settingsTranslations } from './settings';
const zhcnTranslations: Translations = {
common: commonTranslations,
menu: menuTranslations,
login: loginTranslations,
about: aboutTranslations,
services: servicesTranslations,
maintenance: maintenanceTranslations,
incident: incidentTranslations,
ssl: sslTranslations,
settings: settingsTranslations
};
export default zhcnTranslations;
@@ -0,0 +1,23 @@
import { LoginTranslations } from '../types/login';
export const loginTranslations: LoginTranslations = {
signInToYourAccount: "登录您的账号",
dontHaveAccount: "没有账号?",
createOne: "创建一个",
signInWithGoogle: "使用 Google 登录",
orContinueWith: "或",
email: "邮箱",
password: "密码",
forgot: "忘记密码?",
signIn: "登录",
signingIn: "登录中...",
loginSuccessful: "登录成功",
loginSuccessMessage: "您已成功登录。",
loginFailed: "登录失败",
authenticationFailed: "身份验证失败",
bySigningIn: "通过登录,您同意我们的",
termsAndConditions: "条款与条件",
and: "和",
privacyPolicy: "隐私政策",
};
@@ -0,0 +1,68 @@
import { MaintenanceTranslations } from '../types/maintenance';
export const maintenanceTranslations: MaintenanceTranslations = {
scheduledMaintenance: '计划维护',
scheduledMaintenanceDesc: '查看和管理您系统和服务的计划维护窗口',
upcomingMaintenance: '即将',
ongoingMaintenance: '进行中',
completedMaintenance: '已完成',
createMaintenanceWindow: '创建维护',
totalScheduledHours: '总计划小时',
maintenanceName: '维护名称',
maintenanceStatus: '状态',
scheduledStart: '计划开始时间',
scheduledEnd: '计划结束时间',
affectedServices: '受影响的服务',
impact: '影响',
minor: '次要',
major: '主要',
critical: '关键',
none: '无',
actions: '操作',
scheduled: '计划',
inprogress: '进行中',
completed: '已完成',
cancelled: '已取消',
markAsInProgress: '标记为进行中',
markAsCompleted: '标记为已完成',
markAsCancelled: '标记为已取消',
confirmDelete: '确认删除',
deleteMaintenanceConfirmation: '您确定要删除此维护窗口吗?',
thisActionCannotBeUndone: '此操作无法撤销。',
maintenanceDeleted: '维护已删除',
maintenanceDeletedDesc: '维护窗口已成功删除。',
errorDeletingMaintenance: '删除维护窗口时出错。',
statusUpdated: '状态更新',
maintenanceStatusUpdated: '维护状态已成功更新。',
errorUpdatingMaintenanceStatus: '更新维护状态时出错。',
createMaintenance: '创建维护',
createMaintenanceDesc: '为您的服务计划新的维护窗口',
enterTitle: '输入维护标题',
enterDescription: '输入详细的维护描述',
startTime: '开始时间',
endTime: '结束时间',
selectDate: '选择日期',
enterAffectedServices: '输入受影响的服务',
separateServicesWithComma: '用逗号分隔多个服务',
priority: '优先级',
selectPriority: '选择优先级',
selectStatus: '选择状态',
selectImpact: '选择影响',
notifySubscribers: '通知订阅者',
notifySubscribersDesc: '当此维护开始时,向所有订阅者发送通知',
maintenanceCreated: '创建维护',
maintenanceCreatedDesc: '维护窗口已成功计划。',
errorCreatingMaintenance: '创建维护窗口时出错。',
errorFetchingMaintenanceData: '获取维护数据时出错。',
low: '低',
medium: '中',
high: '高',
created: '创建',
lastUpdated: '最后更新',
subscribersWillBeNotified: '订阅者将在维护开始时收到通知',
noNotifications: '不会发送通知',
noScheduledMaintenance: '没有计划的维护',
noMaintenanceWindows: '没有此时间段的维护窗口。通过点击“创建维护”按钮创建一个。',
maintenanceCreatedSuccess: '维护窗口创建成功',
};
+21
View File
@@ -0,0 +1,21 @@
import { MenuTranslations } from '../types/menu';
export const menuTranslations: MenuTranslations = {
uptimeMonitoring: "Uptime 监控",
instanceMonitoring: "实例监控",
sslDomain: "SSL & 域名",
scheduleIncident: "计划与事件",
operationalPage: "运营页面",
reports: "报告",
regionalMonitoring: "区域监控",
settingPanel: "设置面板",
generalSettings: "一般设置",
userManagement: "用户管理",
notificationSettings: "通知设置",
alertsTemplates: "警报模板",
rolesManagement: "角色管理",
dataRetention: "数据保留",
backupSettings: "备份设置",
aboutSystem: "关于系统",
};
@@ -0,0 +1,12 @@
import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceName: "服务名称",
serviceType: "服务类型",
serviceStatus: "服务状态",
responseTime: "响应时间",
uptime: "Uptime",
lastChecked: "最后检查时间",
noServices: "没有符合您筛选条件的服务。",
};
@@ -0,0 +1,52 @@
import { SettingsTranslations } from '../types/settings';
export const settingsTranslations: SettingsTranslations = {
// Tabs
systemSettings: "系统设置",
mailSettings: "邮件设置",
// System Settings
appName: "应用名称",
appURL: "应用 URL",
senderName: "发送者名称",
senderEmail: "发送者邮箱地址",
hideControls: "隐藏控件",
// Mail Settings
smtpSettings: "SMTP 配置",
smtpEnabled: "启用 SMTP",
smtpHost: "SMTP 主机",
smtpPort: "SMTP 端口",
smtpUsername: "SMTP 用户名",
smtpPassword: "SMTP 密码",
smtpAuthMethod: "认证方法",
enableTLS: "启用 TLS",
localName: "本地名称",
// Test Email
testEmail: "测试邮箱",
sendTestEmail: "发送测试邮箱",
emailTemplate: "邮箱模板",
verification: "验证",
passwordReset: "密码重置",
confirmEmailChange: "确认邮箱变更",
otp: "OTP",
loginAlert: "登录警报",
authCollection: "认证集合",
selectCollection: "选择集合",
toEmailAddress: "收件人邮箱地址",
enterEmailAddress: "输入收件人邮箱地址",
sending: "发送中...",
// Actions and status
save: "保存变更",
saving: "保存中...",
settingsUpdated: "设置已成功更新",
errorSavingSettings: "保存设置时出错",
errorFetchingSettings: "加载设置时出错",
testConnection: "测试连接",
testingConnection: "测试连接中...",
connectionSuccess: "连接成功",
connectionFailed: "连接失败"
};
+116
View File
@@ -0,0 +1,116 @@
import { SSLTranslations } from '../types/ssl';
export const sslTranslations: SSLTranslations = {
// Page and section titles
sslDomainManagement: "SSL & 域名管理",
monitorSSLCertificates: "监控和管理您的域名的 SSL 证书",
addSSLCertificate: "添加 SSL 证书",
editSSLCertificate: "编辑 SSL 证书",
deleteSSLCertificate: "删除 SSL 证书",
sslCertificateDetails: "SSL 证书详情",
detailedInfo: "关于",
viewDetailedInformation: "查看详细信息",
// Status related
valid: "有效",
expiringSoon: "即将过期",
expired: "已过期",
pending: "待处理",
// Statistics and cards
validCertificates: "有效证书",
expiringSoonCertificates: "即将过期",
expiredCertificates: "已过期",
// Form fields
domain: "域",
domainName: "域名",
domainCannotChange: "域名创建后不能更改",
warningThreshold: "警告阈值",
warningThresholdDays: "警告阈值(天)",
expiryThreshold: "过期阈值",
expiryThresholdDays: "过期阈值(天)",
notificationChannel: "通知渠道",
chooseChannel: "选择通知渠道",
whereToSend: "通知渠道",
daysBeforeExpiration: "过期前警告天数",
daysBeforeCritical: "过期前关键警告天数",
getNotifiedExpiration: "获取证书即将过期的通知",
getNotifiedCritical: "获取证书即将过期的关键通知",
// Table headers and fields
issuer: "发行机构",
expirationDate: "过期日期",
daysLeft: "剩余天数",
status: "状态",
lastNotified: "最后通知时间",
actions: "操作",
validFrom: "有效从",
validUntil: "有效至",
validityDays: "有效期天数",
validityPeriod: "有效期",
organization: "组织",
commonName: "通用名称",
serialNumber: "序列号",
algorithm: "算法",
subjectAltNames: "主题备用名称",
subjectAlternativeNames: "主题备用名称",
resolvedIP: "解析 IP",
issuedTo: "颁发给",
days: "天",
// Buttons and actions
addDomain: "添加域名",
refreshAll: "刷新所有",
cancel: "取消",
addCertificate: "添加证书",
check: "检查",
view: "查看",
edit: "编辑",
delete: "删除",
close: "关闭",
saveChanges: "保存更改",
updating: "更新中",
// Sections in detail view
basicInformation: "基本信息",
validity: "有效期",
issuerInfo: "发行机构信息",
technicalDetails: "技术详情",
technicalInformation: "技术信息",
monitoringConfig: "监控配置",
monitoringConfiguration: "监控配置",
recordInfo: "记录信息",
certificateDetails: "证书详情",
// Notifications and messages
sslCertificateAdded: "SSL 证书添加成功",
sslCertificateUpdated: "SSL 证书更新成功",
sslCertificateDeleted: "SSL 证书删除成功",
sslCertificateRefreshed: "SSL 证书为 {domain} 刷新成功",
allCertificatesRefreshed: "所有 {count} 证书刷新成功",
someCertificatesFailed: "{success} 证书刷新成功,{failed} 失败",
failedToAddCertificate: "添加 SSL 证书失败",
failedToLoadCertificates: "加载 SSL 证书失败",
failedToUpdateCertificate: "更新 SSL 证书失败",
failedToDeleteCertificate: "删除 SSL 证书失败",
failedToCheckCertificate: "检查 SSL 证书失败",
noCertificatesToRefresh: "没有证书可刷新",
startingRefreshAll: "开始刷新 {count} 证书",
checkingSSLCertificate: "检查 SSL 证书...",
deleteConfirmation: "确定要删除 {domain} 的证书吗?",
deleteWarning: "此操作无法撤销。这将永久删除证书。",
// Misc
unknown: "未知",
never: "从不",
none: "无",
loadingChannels: "加载通知渠道...",
noChannelsFound: "未找到通知渠道",
noSSLCertificates: "未找到 SSL 证书",
created: "创建时间",
lastUpdated: "最后更新时间",
lastNotification: "最后通知时间",
collectionId: "集合 ID"
};
+2
View File
@@ -21,6 +21,8 @@ export interface Server {
template_id: string;
threshold_id: string;
notification_id: string;
notification_status: boolean;
max_retries: number;
timestamp: string;
connection: string;
agent_status: string;
+8
View File
@@ -1,3 +1,4 @@
export interface SSLCertificate {
id: string;
domain: string;
@@ -15,6 +16,10 @@ export interface SSLCertificate {
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
alert_template?: string; // New field for SSL alert template
// PocketBase specific fields
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
last_notified?: string;
created?: string;
updated?: string;
@@ -33,5 +38,8 @@ export interface AddSSLCertificateDto {
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
alert_template?: string; // New field for SSL alert template
notification_id?: string; // Multi notification channels as comma-separated string
template_id?: string; // Alert template ID for PocketBase
check_interval?: number; // New field for check interval in days
}
+5 -5
View File
@@ -2,12 +2,12 @@
import { toast } from "@/hooks/use-toast";
export const copyToClipboard = async (text: string) => {
console.log('copyToClipboard called with text:', text); // Debug log
// console.log('copyToClipboard called with text:', text); // Debug log
try {
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
console.log('Using modern clipboard API'); // Debug log
// console.log('Using modern clipboard API'); // Debug log
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
@@ -16,7 +16,7 @@ export const copyToClipboard = async (text: string) => {
return;
}
console.log('Using fallback clipboard method'); // Debug log
// console.log('Using fallback clipboard method'); // Debug log
// Fallback for older browsers or non-secure contexts
const textArea = document.createElement("textarea");
@@ -39,7 +39,7 @@ export const copyToClipboard = async (text: string) => {
document.body.removeChild(textArea);
if (successful) {
console.log('Copy successful with execCommand'); // Debug log
// console.log('Copy successful with execCommand'); // Debug log
toast({
title: "Copied!",
description: "Content copied to clipboard successfully.",
@@ -48,7 +48,7 @@ export const copyToClipboard = async (text: string) => {
throw new Error('Copy command failed');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// console.error('Failed to copy to clipboard:', error);
// Show error toast
toast({
+35
View File
@@ -0,0 +1,35 @@
name: checkcle-server-agent
services:
checkcle-server-agent:
container_name: monitoring-agent
restart: unless-stopped
ports:
- "8081:8081"
group_add:
- 999
environment:
AGENT_ID: agent_124343535
SERVER_NAME: "test"
SERVER_TOKEN: srv_123243434
POCKETBASE_URL: http://x.x.x.x:8090
POCKETBASE_ENABLED: "true"
volumes:
- /proc:/host/proc:ro
- /etc:/host/etc:ro
- /sys:/host/sys:ro
- /:/host/root:ro
- /var/run:/host/var/run:ro
- /dev:/host/dev:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- monitoring_data:/var/lib/monitoring-agent
- monitoring_logs:/var/log/monitoring-agent
image: operacle/checkcle-server-agent:latest
volumes:
monitoring_data:
external: true
name: monitoring_data
monitoring_logs:
external: true
name: monitoring_logs

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