31 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 80a2eb383d Merge pull request #112 from operacle/develop
feat: add docker compose for server docker agent and Fix: Add tar.gz fallback for unsupported systems
and other improvement
2025-07-31 04:10:06 +07:00
Tola Leng 27ee133e92 Merge pull request #84 from operacle/develop
Updated GitHub issue templates for Bug Report, Help Request,...
2025-07-23 22:56:43 +07:00
Tola Leng af64ac31ea Merge pull request #83 from operacle/develop
Fix: Display new service immediately
2025-07-23 22:29:48 +07:00
Tola Leng 72984f5da4 Merge pull request #79 from operacle/develop
feat: Add create instance monitoring agent and Add Uptime Heatmap to service detail page
2025-07-22 16:08:14 +07:00
Tola Leng cc132a66d9 Merge pull request #70 from operacle/develop
- Improve service listing to display all records correctly
- Fetch all data and handle pagination for better UX
- Improve SSL Certs listing to display all records correctly
- Added ISSUE TEMPLATE 
- Added CODE OF CONDUCT
2025-07-15 22:18:48 +07:00
Tola Leng 3f975db568 Merge pull request #68 from operacle/develop
- feat: Implement notification channels based on status (Service dialog form) and refactor Service form into smaller components
- feat: Allow multiple selections for regional agents and support multiple notifications in the Service form
- feat: Add Japanese language support. Thank you @gnsworks for the contribution!
2025-07-13 18:44:44 +07:00
Tola Leng 547a4c4041 Merge pull request #65 from operacle/develop
feat: Implement notification channel based on status(multi selection) and Refactor Split ServiceForm into smaller components.
2025-07-10 22:17:02 +07:00
Tola Leng e29f60447d Merge pull request #64 from operacle/develop
feat: Improve server overview cards and fix regional_status to Service type
2025-07-08 22:03:15 +07:00
Tola Leng ad869c17aa add codeowners and pull request workflow 2025-07-03 21:03:09 +07:00
Tola Leng 6702bfa5e3 Merge pull request #62 from operacle/develop
Fix: Instance monitoring dashboard styling and Improved docker compose configuration
2025-07-03 20:59:59 +07:00
Tola Leng cb6946b7dd Create FUNDING.yml 2025-07-03 20:56:11 +07:00
Tola Leng 8bd8ae25e6 Merge pull request #61 from operacle/develop
Fix: Server Historical Performance Metrics date filter and improved the container entrypoint script pipeline
2025-07-02 20:05:50 +07:00
Tola Leng eddc12018f Merge pull request #60 from operacle/develop
Fix: Server history chart and date filter and Added Distributed Regional Monitoring Dockerfile
2025-07-01 20:07:23 +07:00
Tola Leng 474a439515 Merge pull request #59 from operacle/develop
feat: Implement instance and docker monitoring dashboard and detail metrics page
2025-06-30 23:07:58 +07:00
Tola Leng 9a4cc7c3db Merge pull request #57 from operacle/develop
Refactor: Separate data display by monitoring source and feat: Implement "Regional Monitoring" option
2025-06-26 16:28:32 +07:00
Tola Leng 23f3c5b8ca Merge pull request #53 from operacle/develop
Integrate regional agents in service-operation and Enhance Regional Agent Installation
2025-06-21 21:49:19 +07:00
Tola Leng f88cc32cf9 Merge pull request #52 from operacle/develop
Implement SSL certificate checker in Go instead of TS and feat: Add error message and details columns
2025-06-20 20:24:55 +07:00
Tola Leng 9f4e759b01 Merge pull request #51 from operacle/develop
Implement CheckCle Microservice Operation
2025-06-19 17:51:39 +07:00
Tola Leng 9ed4d4c7ac Merge pull request #50 from operacle/develop
feat: New Create Service Form and  Split UptimeBar.tsx into smaller components
2025-06-19 15:04:07 +07:00
Tola Leng 9a001f6aec Merge pull request #49 from operacle/develop
Remove HTTP service check and Refactor: Split settings API into modules
2025-06-18 22:09:40 +07:00
Tola Leng cfdedb9636 Merge pull request #45 from operacle/develop
Fix: Implement public status page functionality
2025-06-15 20:49:51 +07:00
Tola Leng 9d5fa805c9 Merge pull request #42 from operacle/develop
Implement password reset functionality.
2025-06-12 17:15:08 +07:00
Tola Leng 68a4ad5478 Merge pull request #38 from operacle/develop
Refactor: Split Sidebar.tsx into smaller components
2025-06-08 21:46:27 +07:00
Tola Leng 9b1918e25a Merge pull request #36 from operacle/develop
feat: Implement Operational and Public Status Page Feature
2025-06-06 19:23:54 +07:00
Tola Leng 1dc25cc461 Merge pull request #35 from operacle/develop
feat: Add mail test functionality
2025-06-06 16:48:46 +07:00
Tola Leng 4dc005c7e4 Merge pull request #33 from operacle/develop
Add SMTP password field in Mail Settings and  Fix: Ensure Add Service dialog respects theme
2025-06-05 17:40:17 +07:00
Tola Leng 4ec0c7a8fa Merge pull request #32 from operacle/develop
Fix: Improve email change flow and auto-logout and feat: Implement uptime monitoring data retention
2025-06-04 16:33:42 +07:00
Tola Leng dd758b9334 Merge pull request #27 from operacle/develop
feat: Implement uptime monitoring data retention
2025-05-31 17:44:43 +07:00
Tola Leng aec7118496 Merge pull request #25 from operacle/develop
feat: Auto logout after password change
2025-05-28 15:37:53 +07:00
Tola Leng 759c3249e1 Merge pull request #22 from operacle/develop
feat: release maintenance and incident management features; apply hotfixes
2025-05-26 21:57:45 +07:00
154 changed files with 28009 additions and 18772 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: #[tolaleng] github: [tolaleng]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: #checkcle open_collective: #checkcle
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
+1 -2
View File
@@ -85,8 +85,7 @@ body:
id: terms id: terms
attributes: attributes:
label: Checklist label: Checklist
description: | description: Please confirm the following before submitting:
Please confirm the following before submitting:
options: options:
- label: I have searched existing issues to avoid creating duplicates. - label: I have searched existing issues to avoid creating duplicates.
required: true required: true
+1 -2
View File
@@ -51,8 +51,7 @@ body:
id: checklist id: checklist
attributes: attributes:
label: Checklist label: Checklist
description: | description: Before submitting, please confirm:
Before submitting, please confirm:
options: options:
- label: I have searched existing issues to make sure this feature hasnt been requested yet. - label: I have searched existing issues to make sure this feature hasnt been requested yet.
required: true required: true
+1 -2
View File
@@ -45,8 +45,7 @@ body:
id: checklist id: checklist
attributes: attributes:
label: Checklist label: Checklist
description: | description: Before submitting, please confirm the following:
Before submitting, please confirm the following:
options: options:
- label: I have searched existing issues and discussions for a solution. - label: I have searched existing issues and discussions for a solution.
required: true required: true
-2
View File
@@ -22,8 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
pocketbase.exe
application/src/lib/pocketbase.ts application/src/lib/pocketbase.ts
server/pb_data/
Dockerfile Dockerfile
application/src/lib/pocketbase-prod.ts application/src/lib/pocketbase-prod.ts
+19 -17
View File
@@ -128,30 +128,32 @@ docker run -d \
--- ---
## Sponsors ## Sponsors
Sponsorships will no longer be accepted. From now on, support will only be accepted in the form of ecosystem and community partnerships that provide infrastructure such as cloud servers, domains, or hosting credits. 🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
If youre a tech company interested in supporting CheckCle, please contact the author directly at tolaleng@checkcle.io ### 🥈 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 ### 🤝 Ecosystem & Community Partner
<div style="display: flex; align-items: center; gap: 10px;"> <a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
<a href="https://github.com/gitbookio">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4" <img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75" width="75" height="75"
style="border-radius: 50%;" style="border-radius: 50%;" />
alt="GitBook Logo" /> </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> </a>
<a href="https://www.cloudflare.com">
<img src="https://cdn.checkcle.io/images/sponsor/cloudflare-checkcle_logo.png"
height="60"
alt="Cloudflare Logo" />
</a>
<a href="https://m.do.co/c/0c27ef82475f">
<img src="https://cdn.checkcle.io/images/sponsor/digitalocean_checkcle.png"
height="50"
alt="DigitalOcean Logo" />
</a>
</div>
--- ---
+4 -8
View File
@@ -1,29 +1,25 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle - Dev</title> <title>CheckCle</title>
<meta name="description" <meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" /> <meta name="author" content="Tola Leng" />
<meta property="og:title" content="checkcle-an-open-source" /> <meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description" <meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" /> <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:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" /> <meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" /> <meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
+15 -469
View File
@@ -8,7 +8,6 @@
"name": "vite_react_shadcn_ts", "name": "vite_react_shadcn_ts",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
@@ -28,7 +27,7 @@
"@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "^1.1.0",
@@ -44,8 +43,6 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"pocketbase": "^0.19.0", "pocketbase": "^0.19.0",
@@ -94,10 +91,13 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.3", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
"license": "MIT", "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -645,21 +645,6 @@
"@floating-ui/utils": "^0.2.8" "@floating-ui/utils": "^0.2.8"
} }
}, },
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": { "node_modules/@floating-ui/react-dom": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -679,26 +664,6 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@headlessui/react": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
@@ -1846,12 +1811,12 @@
} }
}, },
"node_modules/@radix-ui/react-separator": { "node_modules/@radix-ui/react-separator": {
"version": "1.1.7", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.1.3" "@radix-ui/react-primitive": "2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@@ -1868,62 +1833,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider": { "node_modules/@radix-ui/react-slider": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
@@ -2316,103 +2225,6 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-aria/focus": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.5",
"@react-aria/utils": "^3.30.1",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.25.5",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.30.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.10.8",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.20.0", "version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -2862,15 +2674,6 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@swc/types": { "node_modules/@swc/types": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
@@ -2935,33 +2738,6 @@
"react": "^18 || ^19" "react": "^18 || ^19"
} }
}, },
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -3056,13 +2832,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.12", "version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
@@ -3084,13 +2853,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0", "version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@@ -3444,18 +3206,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.20", "version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -3500,16 +3250,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3578,18 +3318,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3630,26 +3358,6 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4135,18 +3843,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4160,16 +3856,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4378,16 +4064,6 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4748,12 +4424,6 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4977,20 +4647,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5192,33 +4848,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jspdf": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.7",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5579,13 +5208,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5828,16 +5450,6 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -6096,11 +5708,10 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.13.11", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
@@ -6139,16 +5750,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.24.0", "version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
@@ -6282,16 +5883,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -6448,22 +6039,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "2.5.4", "version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@@ -6520,16 +6095,6 @@
"tailwindcss": ">=3.0.0 || insiders" "tailwindcss": ">=3.0.0 || insiders"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6743,31 +6308,12 @@
} }
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": { "node_modules/vaul": {
"version": "0.9.9", "version": "0.9.9",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
-2
View File
@@ -11,7 +11,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
@@ -46,7 +45,6 @@
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"framer-motion": "12.23.12",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"jspdf": "^3.0.1", "jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2", "jspdf-autotable": "^5.0.2",
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

+41 -135
View File
@@ -1,40 +1,37 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from '@/components/ui/sonner';
import { ThemeProvider } from "./contexts/ThemeContext"; import { ThemeProvider } from './contexts/ThemeContext';
import { LanguageProvider } from "./contexts/LanguageContext"; import { LanguageProvider } from './contexts/LanguageContext';
import { SidebarProvider } from "./contexts/SidebarContext"; import { SidebarProvider } from './contexts/SidebarContext';
import Index from "./pages/Index"; import Index from './pages/Index';
import Login from "./pages/Login"; import Login from './pages/Login';
import Dashboard from "./pages/Dashboard"; import Dashboard from './pages/Dashboard';
import InstanceMonitoring from "./pages/InstanceMonitoring"; import InstanceMonitoring from './pages/InstanceMonitoring';
import ContainerMonitoring from "./pages/ContainerMonitoring"; import ContainerMonitoring from './pages/ContainerMonitoring';
import ServiceDetail from "./pages/ServiceDetail"; import ServiceDetail from './pages/ServiceDetail';
import SslDomain from "./pages/SslDomain"; import SslDomain from './pages/SslDomain';
import ScheduleIncident from "./pages/ScheduleIncident"; import ScheduleIncident from './pages/ScheduleIncident';
import OperationalPage from "./pages/OperationalPage"; import OperationalPage from './pages/OperationalPage';
import RegionalMonitoring from "./pages/RegionalMonitoring"; import RegionalMonitoring from './pages/RegionalMonitoring';
import Settings from "./pages/Settings"; import Settings from './pages/Settings';
import Profile from "./pages/Profile"; import Profile from './pages/Profile';
import NotFound from "./pages/NotFound"; import NotFound from './pages/NotFound';
import PublicStatusPage from "./pages/PublicStatusPage"; import PublicStatusPage from './pages/PublicStatusPage';
import { ProtectedRoute } from "./components/auth/ProtectedRoute"; import { ProtectedRoute } from './components/auth/ProtectedRoute';
import ServerDetail from "./pages/ServerDetail"; import ServerDetail from './pages/ServerDetail';
function App() { function App() {
const [queryClient] = useState( const [queryClient] = useState(() => new QueryClient({
() =>
new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 60 * 1000, staleTime: 60 * 1000
}, }
}, }
}) }));
);
return ( return (
<BrowserRouter> <BrowserRouter>
@@ -49,110 +46,19 @@ function App() {
{/* Protected routes */} {/* Protected routes */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route <Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
path="/" <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
element={ <Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
<ProtectedRoute> <Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
<Index /> <Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
</ProtectedRoute> <Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
} <Route path="/service/:id" element={<ProtectedRoute><ServiceDetail /></ProtectedRoute>} />
/> <Route path="/ssl-domain" element={<ProtectedRoute><SslDomain /></ProtectedRoute>} />
<Route <Route path="/schedule-incident" element={<ProtectedRoute><ScheduleIncident /></ProtectedRoute>} />
path="/dashboard" <Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
element={ <Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
<ProtectedRoute> <Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Dashboard /> <Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
</ProtectedRoute>
}
/>
<Route
path="/instance-monitoring"
element={
<ProtectedRoute>
<InstanceMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/server-detail/:serverId"
element={
<ProtectedRoute>
<ServerDetail />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring/:serverId"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route
path="/regional-monitoring"
element={
<ProtectedRoute>
<RegionalMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>
@@ -1,3 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
@@ -6,7 +7,6 @@ import { StatusCards } from "./StatusCards";
import { ServiceFilters } from "./ServiceFilters"; import { ServiceFilters } from "./ServiceFilters";
import { ServicesTable } from "./ServicesTable"; import { ServicesTable } from "./ServicesTable";
import { AddServiceDialog } from "@/components/services/AddServiceDialog"; import { AddServiceDialog } from "@/components/services/AddServiceDialog";
import { useLanguage } from "@/contexts/LanguageContext";
interface DashboardContentProps { interface DashboardContentProps {
services: Service[]; services: Service[];
@@ -14,24 +14,16 @@ interface DashboardContentProps {
error: Error | null; error: Error | null;
} }
export const DashboardContent = ({ export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
services,
isLoading,
error,
}: DashboardContentProps) => {
const { t } = useLanguage();
const [filter, setFilter] = useState<string>("all"); const [filter, setFilter] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>(""); const [searchTerm, setSearchTerm] = useState<string>("");
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false); const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
// Filter services based on search term and type filter // Filter services based on search term and type filter
const filteredServices = services.filter((service) => { const filteredServices = services.filter(service => {
const matchesSearch = const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
service.name.toLowerCase().includes(searchTerm.toLowerCase()) || (service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
(service.url && const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter =
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
return matchesSearch && matchesFilter; return matchesSearch && matchesFilter;
}); });
@@ -39,23 +31,21 @@ export const DashboardContent = ({
return ( return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground"> <div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading service data.</p> <p>Error loading service data.</p>
<Button onClick={() => window.location.reload()}>{t("retry")}</Button> <Button onClick={() => window.location.reload()}>Retry</Button>
</div> </div>
); );
} }
return ( return (
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 "> <main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl "> <div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground"> <h2 className="text-2xl font-bold text-foreground">Overview</h2>
{t("overview")}
</h2>
<Button <Button
className="text-primary-foreground" className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)} onClick={() => setIsAddDialogOpen(true)}
> >
<Plus className="w-4 h-4 mr-2" /> {t("newService")} <Plus className="w-4 h-4 mr-2" /> New Service
</Button> </Button>
</div> </div>
+32 -186
View File
@@ -1,39 +1,13 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService"; import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
Moon,
PanelLeft,
PanelLeftClose,
Sun,
Globe,
FileText,
Github,
Twitter,
MessageSquare,
Bell,
User,
Settings,
LogOut,
CaseUpper,
BookA,
CaseSensitive,
CaseLower,
ALargeSmall,
} from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings"; import { useSystemSettings } from "@/hooks/useSystemSettings";
import useDynamicFontSize from "@/contexts/FontSizeContext";
interface HeaderProps { interface HeaderProps {
currentUser: AuthUser | null; currentUser: AuthUser | null;
@@ -46,7 +20,7 @@ export const Header = ({
currentUser, currentUser,
onLogout, onLogout,
sidebarCollapsed, sidebarCollapsed,
toggleSidebar, toggleSidebar
}: HeaderProps) => { }: HeaderProps) => {
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage(); const { language, setLanguage, t } = useLanguage();
@@ -81,35 +55,27 @@ export const Header = ({
}, [currentUser]); }, [currentUser]);
// Prepare avatar URL - ensure it displays correctly if it's a local profile image // Prepare avatar URL - ensure it displays correctly if it's a local profile image
let avatarUrl = ""; let avatarUrl = '';
if (currentUser?.avatar) { if (currentUser?.avatar) {
// If it's a relative path from the public folder, make sure it's resolved properly // If it's a relative path from the public folder, make sure it's resolved properly
if (currentUser.avatar.startsWith("/upload/profile/")) { if (currentUser.avatar.startsWith('/upload/profile/')) {
avatarUrl = currentUser.avatar; avatarUrl = currentUser.avatar;
} else { } else {
avatarUrl = currentUser.avatar; avatarUrl = currentUser.avatar;
} }
console.log("Final avatar URL:", avatarUrl); console.log("Final avatar URL:", avatarUrl);
} }
const [fontSize, setFontSize] = useState(() => {
const savedFontSize = localStorage.getItem("fontSize");
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
});
useDynamicFontSize(fontSize);
return ( return (
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden"> <header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
{/* Grid Pattern Overlay - Similar to StatusCards */} {/* Grid Pattern Overlay - Similar to StatusCards */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<div <div
className="w-full h-full dark:bg-black" className="w-full h-full"
style={{ style={{
backgroundImage: `linear-gradient(${ backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
theme === "dark" ? "#ffffff10" : "#00000010" linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
} 1px, transparent 1px), backgroundSize: '20px 20px'
linear-gradient(90deg, ${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
}} }}
> >
<div className="w-full h-full backdrop-blur-[1px]"></div> <div className="w-full h-full backdrop-blur-[1px]"></div>
@@ -118,100 +84,42 @@ export const Header = ({
{/* Header Content */} {/* Header Content */}
<div className="flex items-center gap-4 z-10"> <div className="flex items-center gap-4 z-10">
<div <Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${ {sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
!sidebarCollapsed ? "hidden" : ""
}`}
>
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className={`h-6 w-6 `}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="mr-2"
>
{sidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</Button> </Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h1 className="font-medium text-xl"> <h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
{greeting},{" "}
{currentUser?.name || currentUser?.email?.split("@")[0] || "User"}{" "}
👋
</h1>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4 z-10"> <div className="flex items-center space-x-4 z-10">
<Button <Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}>
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={toggleTheme}
>
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
{theme === "dark" ? ( {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span> <span className="sr-only">{t("language")}</span>
<Globe className="w-4 h-4" /> <Globe className="w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
onClick={() => setLanguage("en")}
className={language === "en" ? "bg-accent" : ""}
>
{t("english")} {t("english")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
onClick={() => setLanguage("km")}
className={language === "km" ? "bg-accent" : ""}
>
{t("khmer")} {t("khmer")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
onClick={() => setLanguage("de")}
className={language === "de" ? "bg-accent" : ""}
>
{t("Deutsch")} {t("Deutsch")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
onClick={() => setLanguage("ko")}
className={language === "ko" ? "bg-accent" : ""}
>
{t("korean")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("ja")}
className={language === "ja" ? "bg-accent" : ""}
>
{t("japanese")} {t("japanese")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
onClick={() => setLanguage("zhcn")}
className={language === "zhcn" ? "bg-accent" : ""}
>
{t("simplifiedChinese")} {t("simplifiedChinese")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -233,9 +141,7 @@ export const Header = ({
variant="outline" variant="outline"
size="icon" size="icon"
className="rounded-full w-8 h-8 border-border" className="rounded-full w-8 h-8 border-border"
onClick={() => onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
window.open("https://github.com/operacle/checkcle", "_blank")
}
> >
<span className="sr-only">GitHub</span> <span className="sr-only">GitHub</span>
<Github className="w-4 h-4" /> <Github className="w-4 h-4" />
@@ -264,87 +170,30 @@ export const Header = ({
</Button> </Button>
{/* Notifications */} {/* Notifications */}
<Button <Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("notifications")}</span> <span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" /> <Bell className="w-4 h-4" />
</Button> </Button>
{/* Fond Size */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<BookA className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev + 1)}
className={`cursor-pointer`}
>
<span className={`text-[15px] font-semibold`}> Aa +</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev - 1)}
className={`cursor-pointer`}
>
<span className={`text-[12px] font-semibold`}> Aa -</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* User Profile Dropdown */} {/* User Profile Dropdown */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground"> <Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
{avatarUrl ? ( {avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
<AvatarImage {currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
src={avatarUrl} </AvatarFallback>}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar> </Avatar>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-3 p-2"> <div className="flex items-center gap-3 p-2">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
{avatarUrl ? ( {avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
<AvatarImage {currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
src={avatarUrl} </AvatarFallback>}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar> </Avatar>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<span className="font-medium"> <span className="font-medium">{currentUser?.name || 'User'}</span>
{currentUser?.name || "User"} <span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
</span>
<span className="text-xs text-muted-foreground truncate">
{currentUser?.email}
</span>
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -357,10 +206,7 @@ export const Header = ({
<span>{t("settings")}</span> <span>{t("settings")}</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500">
onClick={onLogout}
className="text-red-500 focus:text-red-500"
>
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>{t("logout")}</span> <span>{t("logout")}</span>
</DropdownMenuItem> </DropdownMenuItem>
@@ -1,14 +1,8 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceFiltersProps { interface ServiceFiltersProps {
filter: string; filter: string;
@@ -23,26 +17,23 @@ export const ServiceFilters = ({
setFilter, setFilter,
searchTerm, searchTerm,
setSearchTerm, setSearchTerm,
servicesCount, servicesCount
}: ServiceFiltersProps) => { }: ServiceFiltersProps) => {
const { t } = useLanguage();
return ( return (
<div className="mb-6 flex justify-between items-center"> <div className="mb-6 flex justify-between items-center">
<div className="flex items-center"> <div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground"> <h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
{t("currentlyMonitoring")}
</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm"> <span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount} {servicesCount}
</span> </span>
</div> </div>
<div className="flex space-x-4"> <div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}> <Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800"> <SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder={t("allTypes")} /> <SelectValue placeholder="All Types" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">{t("allTypes")}</SelectItem> <SelectItem value="all">All Types</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem> <SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem> <SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem> <SelectItem value="TCP">TCP</SelectItem>
@@ -52,7 +43,7 @@ export const ServiceFilters = ({
<div className="relative"> <div className="relative">
<Input <Input
className="w-72 bg-card border-border" className="w-72 bg-card border-border"
placeholder={t("search")} placeholder="Search"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
@@ -1,3 +1,4 @@
import React from "react"; import React from "react";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader"; import { SidebarHeader } from "./sidebar/SidebarHeader";
@@ -14,21 +15,14 @@ export const Sidebar = ({ collapsed }: SidebarProps) => {
return ( return (
<div <div
className={` className={`
${ ${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
theme === "dark"
? "bg-black border-[#1e1e1e]"
: "bg-sidebars border-sidebar-border"
}
border-r flex flex-col h-full border-r flex flex-col h-full
${collapsed ? "w-16" : "w-64"} ${collapsed ? 'w-16' : 'w-64'}
${collapsed ? "hidden" : ""}`} `}
> >
<SidebarHeader collapsed={collapsed} /> <SidebarHeader collapsed={collapsed} />
<MainNavigation collapsed={collapsed} /> <MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} /> <SettingsPanel collapsed={collapsed} />
<div className="items-center ml-2 p-2 h-30 border-t ">
Version: latest
</div>
</div> </div>
); );
}; };
@@ -3,15 +3,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react"; import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
import { Service } from "@/services/serviceService"; import { Service } from "@/services/serviceService";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface StatusCardsProps { interface StatusCardsProps {
services: Service[]; services: Service[];
} }
export const StatusCards = ({ services }: StatusCardsProps) => { export const StatusCards = ({ services }: StatusCardsProps) => {
const { t } = useLanguage();
// Count services by status // Count services by status
const upServices = services.filter(s => s.status === "up").length; const upServices = services.filter(s => s.status === "up").length;
const downServices = services.filter(s => s.status === "down").length; const downServices = services.filter(s => s.status === "down").length;
@@ -45,7 +42,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div> ></div>
</div> </div>
<CardHeader className="pb-2 relative z-10"> <CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle> <CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex items-center justify-between relative z-10"> <CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{upServices}</span> <span className="text-5xl font-bold text-white">{upServices}</span>
@@ -77,7 +74,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div> ></div>
</div> </div>
<CardHeader className="pb-2 relative z-10"> <CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle> <CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex items-center justify-between relative z-10"> <CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{downServices}</span> <span className="text-5xl font-bold text-white">{downServices}</span>
@@ -109,7 +106,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div> ></div>
</div> </div>
<CardHeader className="pb-2 relative z-10"> <CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle> <CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex items-center justify-between relative z-10"> <CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{pausedServices}</span> <span className="text-5xl font-bold text-white">{pausedServices}</span>
@@ -141,7 +138,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div> ></div>
</div> </div>
<CardHeader className="pb-2 relative z-10"> <CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle> <CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex items-center justify-between relative z-10"> <CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{warningServices}</span> <span className="text-5xl font-bold text-white">{warningServices}</span>
@@ -1,3 +1,4 @@
import React from "react"; import React from "react";
import { MenuItem } from "./MenuItem"; import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData"; import { mainMenuItems } from "./navigationData";
@@ -6,15 +7,9 @@ interface MainNavigationProps {
collapsed: boolean; collapsed: boolean;
} }
export const MainNavigation: React.FC<MainNavigationProps> = ({ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
collapsed,
}) => {
return ( return (
<nav <nav className="my-2 mx-1 py-1 px-1">
className={`my-2 mx-1 py-1 px-1 dark:bg-black ${
collapsed ? "hidden" : ""
}`}
>
{mainMenuItems.map((item) => ( {mainMenuItems.map((item) => (
<MenuItem <MenuItem
key={item.id} key={item.id}
@@ -1,3 +1,4 @@
import React from "react"; import React from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
@@ -21,7 +22,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
translationKey, translationKey,
color, color,
hasNavigation, hasNavigation,
collapsed, collapsed
}) => { }) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { t } = useLanguage(); const { t } = useLanguage();
@@ -43,24 +44,18 @@ export const MenuItem: React.FC<MenuItemProps> = ({
return ( return (
<div <div
className={` className={`
${collapsed ? "p-3" : "p-2 pl-3"} ${collapsed ? 'p-3' : 'p-2 pl-3'}
mb-1 rounded-lg mb-1 rounded-lg
${ ${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
isActive
? theme === "dark"
? "bg-drak"
: "bg-sidebar-accent "
: `hover:${theme === "dark" ? "bg-gray-800" : "bg-sidebar-accent "}`
}
flex items-center flex items-center
${collapsed ? "justify-center" : ""} ${collapsed ? 'justify-center' : ''}
cursor-pointer cursor-pointer
`} `}
onClick={handleClick} onClick={handleClick}
> >
<Icon className={`${mainIconSize} ${color}`} /> <Icon className={`${mainIconSize} ${color}`} />
{!collapsed && ( {!collapsed && (
<span className="ml-2.5 font-medium text-foreground tracking-wide "> <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
{t(translationKey)} {t(translationKey)}
</span> </span>
)} )}
@@ -1,13 +1,10 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { Settings, ChevronDown } from "lucide-react"; import { Settings, ChevronDown } from "lucide-react";
import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { settingsMenuItems } from "./navigationData"; import { settingsMenuItems } from "./navigationData";
@@ -20,27 +17,19 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>( const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
"general" const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
);
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
const savedValue = localStorage.getItem("settingsPanelOpen");
// Check if the saved value exists and is "true"
return savedValue === "true";
});
// Update active settings item based on URL // Update active settings item based on URL
useEffect(() => { useEffect(() => {
if (location.pathname === "/settings") { if (location.pathname === '/settings') {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const panel = params.get("panel"); const panel = params.get('panel');
if (panel) { if (panel) {
setActiveSettingsItem(panel); setActiveSettingsItem(panel);
} }
} }
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString()); }, [location]);
}, [location, settingsPanelOpen]);
const handleSettingsItemClick = (item: string) => { const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item); setActiveSettingsItem(item);
@@ -54,27 +43,15 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
}; };
const getMenuItemClasses = (isActive: boolean) => { const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${ return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
isActive
? theme === "dark"
? "bg-[#1a1a1a]"
: "bg-sidebar-accent"
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
} rounded-lg flex items-center `;
}; };
if (collapsed) { if (collapsed) {
const mainIconSize = "h-6 w-6"; const mainIconSize = "h-6 w-6";
return ( return (
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<div <div
className={`border-t ${ onClick={(e) => handleMenuItemClick('/settings', e)}
theme === "dark"
? "border-[#1e1e1e] bg-[#121212]"
: "border-sidebar-border bg-sidebar"
} p-4 flex justify-center ${collapsed ? "hidden" : ""}`}
>
<div
onClick={(e) => handleMenuItemClick("/settings", e)}
className="cursor-pointer" className="cursor-pointer"
> >
<Settings className={`${mainIconSize} text-purple-400`} /> <Settings className={`${mainIconSize} text-purple-400`} />
@@ -84,59 +61,33 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
} }
return ( return (
<div <div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
className={`flex-1 flex flex-col border-t ${ <Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
theme === "dark" <CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-sidebars"
} p-4 ${collapsed ? "hidden" : ""}`}
>
<Collapsible
open={settingsPanelOpen}
onOpenChange={setSettingsPanelOpen}
className="w-full flex flex-col flex-1"
>
<CollapsibleTrigger
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
}`}
>
<div className="flex items-center"> <div className="flex items-center">
<span className="font-medium tracking-wide"> <span className="font-medium tracking-wide">{t("settingPanel")}</span>
{t("settingPanel")}
</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Settings className="h-4 w-4 mr-1" /> <Settings className="h-4 w-4 mr-1" />
<ChevronDown <ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
className={`h-4 w-4 transition-transform duration-200 ${
settingsPanelOpen ? "rotate-180" : ""
}`}
/>
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent <CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
className={`${
theme === "dark" ? "bg-black" : "bg-sidebars"
} flex-1 flex flex-col`}
>
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1"> <div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<div className="space-y-2 pr-4 cursor-pointer"> <div className="space-y-2 pr-4">
{settingsMenuItems.map((item) => ( {settingsMenuItems.map((item) => (
<div <div
key={item.id} key={item.id}
className={getMenuItemClasses( className={getMenuItemClasses(activeSettingsItem === item.id)}
activeSettingsItem === item.id
)}
onClick={(e) => { onClick={(e) => {
handleMenuItemClick(`/settings?panel=${item.id}`, e); handleMenuItemClick(`/settings?panel=${item.id}`, e);
handleSettingsItemClick(item.id); handleSettingsItemClick(item.id);
}} }}
> >
<item.icon className="h-4 w-4 mr-2" /> <item.icon className="h-4 w-4 mr-2" />
<span className="text-base">{t(item.translationKey)}</span> <span className="text-sm">{t(item.translationKey)}</span>
</div> </div>
))} ))}
</div> </div>
@@ -1,3 +1,4 @@
import React from "react"; import React from "react";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
@@ -9,15 +10,13 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<div <div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
className={`p-[15.7px] ${ <div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2">
theme === "dark" <img
? "border-[#1e1e1e] bg-black" src="/favicon_sidebar.ico"
: "border-sidebar-border bg-white" alt="CheckCle"
} border-b flex items-center ${collapsed ? "justify-center" : ""}`} className="h-6 w-6"
> />
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
</div> </div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>} {!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div> </div>
@@ -1,17 +1,14 @@
import { useState } from "react"; import { useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
useOperationalPages, import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
useDeleteOperationalPage, import { EditOperationalPageDialog } from './EditOperationalPageDialog';
} from "@/hooks/useOperationalPage"; import { OperationalPageCard } from './OperationalPageCard';
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog"; import { Card, CardContent } from '@/components/ui/card';
import { EditOperationalPageDialog } from "./EditOperationalPageDialog"; import { Button } from '@/components/ui/button';
import { OperationalPageCard } from "./OperationalPageCard"; import { OperationalPageRecord } from '@/types/operational.types';
import { Card, CardContent } from "@/components/ui/card"; import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Button } from "@/components/ui/button"; import { Skeleton } from '@/components/ui/skeleton';
import { OperationalPageRecord } from "@/types/operational.types";
import { Activity, Plus, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -21,26 +18,17 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from '@/components/ui/alert-dialog';
export const OperationalPageContent = () => { export const OperationalPageContent = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
data: pages,
isLoading,
error,
refetch,
isRefetching,
} = useOperationalPages();
const deleteMutation = useDeleteOperationalPage(); const deleteMutation = useDeleteOperationalPage();
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>( const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
null
);
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pageToDelete, setPageToDelete] = const [pageToDelete, setPageToDelete] = useState<OperationalPageRecord | null>(null);
useState<OperationalPageRecord | null>(null);
const handleEdit = (page: OperationalPageRecord) => { const handleEdit = (page: OperationalPageRecord) => {
setEditingPage(page); setEditingPage(page);
@@ -49,10 +37,10 @@ export const OperationalPageContent = () => {
const handleView = (page: OperationalPageRecord) => { const handleView = (page: OperationalPageRecord) => {
if (page.custom_domain) { if (page.custom_domain) {
window.open(`https://${page.custom_domain}`, "_blank"); window.open(`https://${page.custom_domain}`, '_blank');
} else { } else {
// Navigate to the public status page route using the correct format // Navigate to the public status page route using the correct format
window.open(`/public/${page.slug}`, "_blank"); window.open(`/public/${page.slug}`, '_blank');
} }
}; };
@@ -68,7 +56,7 @@ export const OperationalPageContent = () => {
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setPageToDelete(null); setPageToDelete(null);
} catch (error) { } catch (error) {
console.error("Error deleting page:", error); console.error('Error deleting page:', error);
} }
} }
}; };
@@ -80,9 +68,7 @@ export const OperationalPageContent = () => {
<div className="mb-4"> <div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" /> <Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div> </div>
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
Failed to load operational pages
</h3>
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
There was an error loading your operational pages. Please try again. There was an error loading your operational pages. Please try again.
</p> </p>
@@ -96,13 +82,11 @@ export const OperationalPageContent = () => {
} }
return ( return (
<div className="w-full container mx-auto px-4 py-8 "> <div className="container mx-auto px-4 py-8">
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight mb-2"> <h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
Operational Pages
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Manage your public status pages and monitor service health Manage your public status pages and monitor service health
</p> </p>
@@ -115,9 +99,7 @@ export const OperationalPageContent = () => {
onClick={() => refetch()} onClick={() => refetch()}
disabled={isRefetching} disabled={isRefetching}
> >
<RefreshCw <RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
/>
Refresh Refresh
</Button> </Button>
<CreateOperationalPageDialog /> <CreateOperationalPageDialog />
@@ -153,12 +135,9 @@ export const OperationalPageContent = () => {
<div className="mb-4"> <div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" /> <Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div> </div>
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
No operational pages found
</h3>
<p className="text-muted-foreground mb-6"> <p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your Create your first operational page to start monitoring your services and communicate status to your users.
services and communicate status to your users.
</p> </p>
<CreateOperationalPageDialog /> <CreateOperationalPageDialog />
</CardContent> </CardContent>
@@ -188,12 +167,12 @@ export const OperationalPageContent = () => {
<span className="font-medium">Total Pages:</span> {pages.length} <span className="font-medium">Total Pages:</span> {pages.length}
</div> </div>
<div> <div>
<span className="font-medium">Public Pages:</span>{" "} <span className="font-medium">Public Pages:</span>{' '}
{pages.filter((p) => p.is_public === "true").length} {pages.filter(p => p.is_public === 'true').length}
</div> </div>
<div> <div>
<span className="font-medium">Operational:</span>{" "} <span className="font-medium">Operational:</span>{' '}
{pages.filter((p) => p.status === "operational").length} {pages.filter(p => p.status === 'operational').length}
</div> </div>
</div> </div>
</div> </div>
@@ -212,9 +191,7 @@ export const OperationalPageContent = () => {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle> <AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete "{pageToDelete?.title}"? This Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
action cannot be undone and will permanently remove the
operational page and all its components.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -224,7 +201,7 @@ export const OperationalPageContent = () => {
className="bg-red-600 hover:bg-red-700" className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
> >
{deleteMutation.isPending ? "Deleting..." : "Delete"} {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -1,13 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react"; import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
import { regionalService } from "@/services/regionalService"; import { regionalService } from "@/services/regionalService";
import { RegionalService } from "@/types/regional.types"; import { RegionalService } from "@/types/regional.types";
@@ -20,29 +15,24 @@ export const RegionalMonitoringContent = () => {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const { data: regionalServices = [], isLoading, error } = useQuery({
data: regionalServices = [], queryKey: ['regional-services'],
isLoading,
error,
} = useQuery({
queryKey: ["regional-services"],
queryFn: regionalService.getRegionalServices, queryFn: regionalService.getRegionalServices,
refetchInterval: 30000, // Refetch every 30 seconds refetchInterval: 30000, // Refetch every 30 seconds
}); });
const handleAgentAdded = () => { const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ["regional-services"] }); queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({ toast({
title: "Regional Agent Added", title: "Regional Agent Added",
description: description: "The regional monitoring agent has been successfully configured.",
"The regional monitoring agent has been successfully configured.",
}); });
}; };
const handleDeleteAgent = async (id: string) => { const handleDeleteAgent = async (id: string) => {
try { try {
await regionalService.deleteRegionalService(id); await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ["regional-services"] }); queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({ toast({
title: "Agent Removed", title: "Agent Removed",
description: "The regional monitoring agent has been removed.", description: "The regional monitoring agent has been removed.",
@@ -56,18 +46,14 @@ export const RegionalMonitoringContent = () => {
} }
}; };
const onlineAgents = regionalServices.filter( const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length;
(agent) => agent.connection === "online"
).length;
const totalAgents = regionalServices.length; const totalAgents = regionalServices.length;
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
Regional Monitoring
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Manage distributed monitoring agents across different regions Manage distributed monitoring agents across different regions
</p> </p>
@@ -99,25 +85,23 @@ export const RegionalMonitoringContent = () => {
<Wifi className="h-4 w-4 text-green-600" /> <Wifi className="h-4 w-4 text-green-600" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-green-600"> <div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
{onlineAgents} <p className="text-xs text-muted-foreground">
</div> Currently connected
<p className="text-xs text-muted-foreground">Currently connected</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
Offline Agents
</CardTitle>
<WifiOff className="h-4 w-4 text-red-600" /> <WifiOff className="h-4 w-4 text-red-600" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-red-600"> <div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
{totalAgents - onlineAgents} <p className="text-xs text-muted-foreground">
</div> Disconnected agents
<p className="text-xs text-muted-foreground">Disconnected agents</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -149,8 +133,7 @@ export const RegionalMonitoringContent = () => {
<MapPin className="h-12 w-12 text-muted-foreground mb-4" /> <MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3> <h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<p className="text-muted-foreground text-center mb-4"> <p className="text-muted-foreground text-center mb-4">
Get started by adding your first regional monitoring agent to Get started by adding your first regional monitoring agent to extend your monitoring coverage.
extend your monitoring coverage.
</p> </p>
<Button onClick={() => setAddDialogOpen(true)}> <Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
@@ -1,26 +1,22 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, CalendarClock, AlertCircle } from "lucide-react"; import { Plus, CalendarClock, AlertCircle } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab"; import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
import { IncidentManagementTab } from "./IncidentManagementTab"; import { IncidentManagementTab } from "./IncidentManagementTab";
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog"; import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog"; import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
import { useToast } from "@/hooks/use-toast"; import { useToast } from '@/hooks/use-toast';
import { import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
initMaintenanceNotifications,
stopMaintenanceNotifications,
} from "@/services/maintenance/maintenanceNotificationService";
export const ScheduleIncidentContent = () => { export const ScheduleIncidentContent = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const { toast } = useToast(); const { toast } = useToast();
const [activeTab, setActiveTab] = useState("maintenance"); const [activeTab, setActiveTab] = useState("maintenance");
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
useState(false); const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0); const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
@@ -52,8 +48,8 @@ export const ScheduleIncidentContent = () => {
// Show success toast // Show success toast
toast({ toast({
title: t("success"), title: t('success'),
description: t("maintenanceCreatedSuccess"), description: t('maintenanceCreatedSuccess'),
}); });
}; };
@@ -65,26 +61,24 @@ export const ScheduleIncidentContent = () => {
// Show success toast // Show success toast
toast({ toast({
title: t("success"), title: t('success'),
description: t("incidentCreatedSuccess"), description: t('incidentCreatedSuccess'),
}); });
}; };
return ( return (
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 "> <main className="flex-1 flex flex-col overflow-auto bg-background p-6">
<div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl"> <div className="flex flex-col gap-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-foreground"> <h2 className="text-2xl font-bold text-foreground">
{t("scheduleIncidentManagement")} {t('scheduleIncidentManagement')}
</h2> </h2>
<Button <Button
className="text-primary-foreground" className="text-primary-foreground"
onClick={handleCreateButtonClick} onClick={handleCreateButtonClick}
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance" {activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
? t("createMaintenanceWindow")
: t("createIncident")}
</Button> </Button>
</div> </div>
@@ -96,11 +90,11 @@ export const ScheduleIncidentContent = () => {
<TabsList className="grid w-full grid-cols-2 mb-4"> <TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="maintenance"> <TabsTrigger value="maintenance">
<CalendarClock className="w-4 h-4 mr-2" /> <CalendarClock className="w-4 h-4 mr-2" />
{t("scheduledMaintenance")} {t('scheduledMaintenance')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="incidents"> <TabsTrigger value="incidents">
<AlertCircle className="w-4 h-4 mr-2" /> <AlertCircle className="w-4 h-4 mr-2" />
{t("incidentManagement")} {t('incidentManagement')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -25,20 +25,19 @@ export const IncidentDetailContent = ({
onClose, onClose,
assignedUser assignedUser
}: IncidentDetailContentProps) => { }: IncidentDetailContentProps) => {
// Fetch assigned user details if none was provided; prefer server field // Fetch assigned user details if one wasn't provided and there's an assigned_to field
const assigneeId = incident?.assigned_users || incident?.assigned_to;
const { data: fetchedUser } = useQuery({ const { data: fetchedUser } = useQuery({
queryKey: ['user', assigneeId], queryKey: ['user', incident?.assigned_to],
queryFn: async () => { queryFn: async () => {
if (!assigneeId) return null; if (!incident?.assigned_to) return null;
try { try {
return await userService.getUser(assigneeId); return await userService.getUser(incident.assigned_to);
} catch (error) { } catch (error) {
console.error("Failed to fetch assigned user:", error); console.error("Failed to fetch assigned user:", error);
return null; return null;
} }
}, },
enabled: !!assigneeId && !assignedUser, enabled: !!incident?.assigned_to && !assignedUser,
staleTime: 300000 // Cache for 5 minutes staleTime: 300000 // Cache for 5 minutes
}); });
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
onOpenChange, onOpenChange,
incident incident
}: IncidentDetailDialogProps) => { }: IncidentDetailDialogProps) => {
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to) // Fetch user data for assigned_to field
const { data: users = [] } = useQuery({ const { data: users = [] } = useQuery({
queryKey: ['users'], queryKey: ['users'],
queryFn: async () => { queryFn: async () => {
@@ -25,12 +25,12 @@ export const IncidentDetailDialog = ({
return Array.isArray(usersList) ? usersList : []; return Array.isArray(usersList) ? usersList : [];
}, },
staleTime: 300000, // Cache for 5 minutes staleTime: 300000, // Cache for 5 minutes
enabled: !!(incident?.assigned_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
}); });
// Find the assigned user (prefer assigned_users, fallback to assigned_to) // Find the assigned user
const assignedUser = (incident?.assigned_users || incident?.assigned_to) const assignedUser = incident?.assigned_to
? users.find(user => user.id === (incident?.assigned_users || incident?.assigned_to)) ? users.find(user => user.id === incident.assigned_to)
: null; : null;
if (!incident) return null; if (!incident) return null;
@@ -23,19 +23,19 @@ export {
// Legacy component - keeping this for backward compatibility with other imports // Legacy component - keeping this for backward compatibility with other imports
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => { export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
// Fetch assigned user details if there's an assigned field (prefer assigned_users, fallback to assigned_to) // Fetch assigned user details if there's an assigned_to field
const { data: assignedUser } = useQuery({ const { data: assignedUser } = useQuery({
queryKey: ['user', incident?.assigned_users || incident?.assigned_to], queryKey: ['user', incident?.assigned_to],
queryFn: async () => { queryFn: async () => {
if (!(incident?.assigned_users || incident?.assigned_to)) return null; if (!incident?.assigned_to) return null;
try { try {
return await userService.getUser(incident?.assigned_users || incident?.assigned_to); return await userService.getUser(incident.assigned_to);
} catch (error) { } catch (error) {
console.error("Failed to fetch assigned user:", error); console.error("Failed to fetch assigned user:", error);
return null; return null;
} }
}, },
enabled: !!(incident?.assigned_users || incident?.assigned_to), enabled: !!incident?.assigned_to,
staleTime: 300000 // Cache for 5 minutes staleTime: 300000 // Cache for 5 minutes
}); });
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
</Avatar> </Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span> <span>{assignedUser.full_name || assignedUser.username}</span>
</div> </div>
) : (incident.assigned_users || incident.assigned_to) ? ( ) : incident.assigned_to ? (
<span>{incident.assigned_users || incident.assigned_to}</span> <span>{incident.assigned_to}</span>
) : ( ) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span> <span className="text-muted-foreground italic">{t('unassigned')}</span>
)} )}
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
</Avatar> </Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span> <span>{assignedUser.full_name || assignedUser.username}</span>
</div> </div>
) : (incident.assigned_users || incident.assigned_to) ? ( ) : incident.assigned_to ? (
<span>{incident.assigned_users || incident.assigned_to}</span> <span>{incident.assigned_to}</span>
) : ( ) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span> <span className="text-muted-foreground italic">{t('unassigned')}</span>
)} )}
@@ -68,7 +68,7 @@ export const IncidentBasicFields: React.FC = () => {
const selectedUser = users.find(user => user.id === form.getValues('assigned_to')); const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
// Function to get user initials from name // Function to get user initials from name
const getUserInitials = (user: { full_name?: string; username: string }): string => { const getUserInitials = (user: any): string => {
if (user.full_name) { if (user.full_name) {
const nameParts = user.full_name.split(' '); const nameParts = user.full_name.split(' ');
if (nameParts.length > 1) { if (nameParts.length > 1) {
@@ -25,7 +25,7 @@ export const useIncidentEditForm = (
impact: (incident.impact?.toLowerCase() || 'minor') as any, impact: (incident.impact?.toLowerCase() || 'minor') as any,
priority: (incident.priority?.toLowerCase() || 'medium') as any, priority: (incident.priority?.toLowerCase() || 'medium') as any,
service_id: incident.service_id || '', service_id: incident.service_id || '',
assigned_to: incident.assigned_users || incident.assigned_to || '', assigned_to: incident.assigned_to || '',
root_cause: incident.root_cause || '', root_cause: incident.root_cause || '',
resolution_steps: incident.resolution_steps || '', resolution_steps: incident.resolution_steps || '',
lessons_learned: incident.lessons_learned || '', lessons_learned: incident.lessons_learned || '',
@@ -45,15 +45,12 @@ export const useIncidentEditForm = (
impact: data.impact, impact: data.impact,
priority: data.priority, priority: data.priority,
service_id: data.service_id, service_id: data.service_id,
...(data.assigned_to assigned_to: data.assigned_to, // This is the user ID from the form
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
: {}),
root_cause: data.root_cause, root_cause: data.root_cause,
resolution_steps: data.resolution_steps, resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned, lessons_learned: data.lessons_learned,
}); });
toast({ toast({
title: t('incidentUpdated'), title: t('incidentUpdated'),
description: t('incidentUpdatedDesc'), description: t('incidentUpdatedDesc'),
@@ -57,7 +57,6 @@ export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
priority: data.priority, priority: data.priority,
service_id: data.service_id, service_id: data.service_id,
assigned_to: data.assigned_to, assigned_to: data.assigned_to,
assigned_users: data.assigned_to, // map to server field
root_cause: data.root_cause, root_cause: data.root_cause,
resolution_steps: data.resolution_steps, resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned, lessons_learned: data.lessons_learned,
@@ -86,7 +86,7 @@ export const IncidentTableRow = memo(({
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<AssignedUserCell userId={localItem.assigned_users || localItem.assigned_to} /> <AssignedUserCell userId={localItem.assigned_to} />
</TableCell> </TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}> <TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-end items-center space-x-2"> <div className="flex justify-end items-center space-x-2">
@@ -1,45 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
DropdownMenu, import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
DropdownMenuContent, import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
RefreshCw,
Search,
Eye,
Activity,
MoreHorizontal,
Pause,
Play,
Edit,
Trash2,
} from "lucide-react";
import { Server } from "@/types/server.types"; import { Server } from "@/types/server.types";
import { ServerStatusBadge } from "./ServerStatusBadge"; import { ServerStatusBadge } from "./ServerStatusBadge";
import { OSTypeIcon } from "./OSTypeIcon"; import { OSTypeIcon } from "./OSTypeIcon";
@@ -55,11 +23,7 @@ interface ServerTableProps {
onRefresh: () => void; onRefresh: () => void;
} }
export const ServerTable = ({ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
servers,
isLoading,
onRefresh,
}: ServerTableProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -70,8 +34,7 @@ export const ServerTable = ({
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const filteredServers = servers.filter( const filteredServers = servers.filter(server =>
(server) =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) || server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) || server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase()) server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
@@ -94,38 +57,35 @@ export const ServerTable = ({
} }
try { try {
setPausingServers((prev) => new Set(prev).add(serverId)); setPausingServers(prev => new Set(prev).add(serverId));
// Only update the status field, preserving all other server configuration // Only update the status field, preserving all other server configuration
const updateData = { const updateData = {
status: isPaused ? "up" : "paused", status: isPaused ? "up" : "paused",
last_checked: new Date().toISOString(), last_checked: new Date().toISOString()
}; };
await pb.collection("servers").update(serverId, updateData); await pb.collection('servers').update(serverId, updateData);
toast({ toast({
title: isPaused ? "Server resumed" : "Server paused", title: isPaused ? "Server resumed" : "Server paused",
description: `Monitoring ${isPaused ? "resumed" : "paused"} for ${ description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
server.name
}`,
}); });
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`); // console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
// Refresh the server list to show updated status // Refresh the server list to show updated status
onRefresh(); onRefresh();
} catch (error) { } catch (error) {
// console.error('Error updating server status:', error); // console.error('Error updating server status:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
description: `Failed to ${ description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
isPaused ? "resume" : "pause"
} server monitoring. Please try again.`,
}); });
} finally { } finally {
setPausingServers((prev) => { setPausingServers(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.delete(serverId); newSet.delete(serverId);
return newSet; return newSet;
@@ -150,7 +110,7 @@ export const ServerTable = ({
setIsDeleting(true); setIsDeleting(true);
// Delete the server from the database // Delete the server from the database
await pb.collection("servers").delete(selectedServer.id); await pb.collection('servers').delete(selectedServer.id);
toast({ toast({
title: "Server deleted", title: "Server deleted",
@@ -163,6 +123,7 @@ export const ServerTable = ({
// Close the dialog // Close the dialog
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setSelectedServer(null); setSelectedServer(null);
} catch (error) { } catch (error) {
// console.error('Error deleting server:', error); // console.error('Error deleting server:', error);
toast({ toast({
@@ -179,61 +140,61 @@ export const ServerTable = ({
value, value,
label, label,
subtitle, subtitle,
type, type
}: { }: {
value: number; value: number;
label: string; label: string;
subtitle: string; subtitle: string;
type: "cpu" | "memory" | "disk"; type: 'cpu' | 'memory' | 'disk'
}) => { }) => {
const getGradientColors = (type: string, value: number) => { const getGradientColors = (type: string, value: number) => {
if (type === "cpu") { if (type === 'cpu') {
if (value > 90) return "from-red-500 to-red-600"; if (value > 90) return 'from-red-500 to-red-600';
if (value > 75) return "from-orange-500 to-orange-600"; if (value > 75) return 'from-orange-500 to-orange-600';
if (value > 60) return "from-yellow-500 to-yellow-600"; if (value > 60) return 'from-yellow-500 to-yellow-600';
return "from-green-500 to-green-600"; return 'from-green-500 to-green-600';
} }
if (type === "memory") { if (type === 'memory') {
if (value > 90) return "from-red-500 to-red-600"; if (value > 90) return 'from-red-500 to-red-600';
if (value > 75) return "from-yellow-500 to-yellow-600"; if (value > 75) return 'from-yellow-500 to-yellow-600';
return "from-blue-500 to-blue-600"; return 'from-blue-500 to-blue-600';
} }
if (type === "disk") { if (type === 'disk') {
if (value > 95) return "from-red-500 to-red-600"; if (value > 95) return 'from-red-500 to-red-600';
if (value > 85) return "from-yellow-500 to-yellow-600"; if (value > 85) return 'from-yellow-500 to-yellow-600';
return "from-orange-500 to-orange-600"; return 'from-orange-500 to-orange-600';
} }
return "from-gray-500 to-gray-600"; return 'from-gray-500 to-gray-600';
}; };
const getTextColor = (value: number) => { const getTextColor = (value: number) => {
if (value > 90) return "text-red-600 dark:text-red-400"; if (value > 90) return 'text-red-600 dark:text-red-400';
if (value > 75) return "text-orange-600 dark:text-orange-400"; if (value > 75) return 'text-orange-600 dark:text-orange-400';
if (value > 60) return "text-yellow-600 dark:text-yellow-400"; if (value > 60) return 'text-yellow-600 dark:text-yellow-400';
return "text-green-600 dark:text-green-400"; return 'text-green-600 dark:text-green-400';
}; };
return ( return (
<div className="space-y-2 min-w-[120px] -mt-20"> <div className="space-y-2 min-w-[120px]">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className={`text-sm font-semibold ${getTextColor(value)}`}> <span className={`text-sm font-semibold ${getTextColor(value)}`}>
{label} {label}
</span> </span>
<span className="text-xs text-muted-foreground">{subtitle} </span> <span className="text-xs text-muted-foreground">
{subtitle}
</span>
</div> </div>
<div className="relative"> <div className="relative">
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner"> <div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
<div <div
className={`h-full bg-gradient-to-r ${getGradientColors( className={`h-full bg-gradient-to-r ${getGradientColors(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
type,
value
)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
style={{ width: `${Math.min(value, 100)}%` }} style={{ width: `${Math.min(value, 100)}%` }}
> >
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse opacity-60" /> <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse opacity-60" />
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
@@ -283,107 +244,27 @@ export const ServerTable = ({
<p className="text-muted-foreground">No servers found</p> <p className="text-muted-foreground">No servers found</p>
</div> </div>
) : ( ) : (
<div <div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
className={`${
theme === "dark" ? "bg-gray-900" : "bg-white"
} rounded-lg border border-border shadow-sm`}
>
<Table> <Table>
<TableHeader <TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
className={`${ <TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
theme === "dark" ? "bg-gray-800" : "bg-gray-50" <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
}`} <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Status</TableHead>
> <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>OS</TableHead>
<TableRow <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>IP Address</TableHead>
className={`${ <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
theme === "dark" <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
? "border-gray-700 hover:bg-gray-800" <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
: "border-gray-200 hover:bg-gray-100" <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
}`} <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Last Checked</TableHead>
> <TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>Actions</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Name
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Status
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
OS
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
IP Address
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
CPU
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Memory
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Disk
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Uptime
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Last Checked
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4 text-right`}
>
Actions
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredServers.map((server) => { {filteredServers.map((server) => {
const cpuUsage = server.cpu_usage || 0; const cpuUsage = server.cpu_usage || 0;
const memoryUsage = const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
server.ram_total > 0 const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
? (server.ram_used / server.ram_total) * 100
: 0;
const diskUsage =
server.disk_total > 0
? (server.disk_used / server.disk_total) * 100
: 0;
const isPaused = server.status === "paused"; const isPaused = server.status === "paused";
const isProcessing = pausingServers.has(server.id); const isProcessing = pausingServers.has(server.id);
@@ -404,10 +285,7 @@ export const ServerTable = ({
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<OSTypeIcon osType={server.os_type} /> <OSTypeIcon osType={server.os_type} />
<span <span className="text-sm truncate" title={server.os_type}>
className="text-sm truncate"
title={server.os_type}
>
{server.os_type} {server.os_type}
</span> </span>
</div> </div>
@@ -429,9 +307,7 @@ export const ServerTable = ({
<CustomProgressBar <CustomProgressBar
value={memoryUsage} value={memoryUsage}
label={`${memoryUsage.toFixed(1)}%`} label={`${memoryUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes( subtitle={serverService.formatBytes(server.ram_total)}
server.ram_total
)}
type="memory" type="memory"
/> />
</TableCell> </TableCell>
@@ -439,17 +315,12 @@ export const ServerTable = ({
<CustomProgressBar <CustomProgressBar
value={diskUsage} value={diskUsage}
label={`${diskUsage.toFixed(1)}%`} label={`${diskUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes( subtitle={serverService.formatBytes(server.disk_total)}
server.disk_total
)}
type="disk" type="disk"
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<div <div className="text-sm truncate" title={server.uptime}>
className="text-sm truncate"
title={server.uptime}
>
{server.uptime} {server.uptime}
</div> </div>
</TableCell> </TableCell>
@@ -458,17 +329,10 @@ export const ServerTable = ({
{new Date(server.last_checked).toLocaleString()} {new Date(server.last_checked).toLocaleString()}
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
className="text-right"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
variant="ghost"
className="h-8 w-8 p-0"
disabled={isProcessing}
>
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
{isProcessing ? ( {isProcessing ? (
<RefreshCw className="h-4 w-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
@@ -477,36 +341,20 @@ export const ServerTable = ({
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent align="end" className="w-[200px]">
align="end" <DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
className="w-[200px]"
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleViewDetails(server.id);
}}
>
<Eye className="mr-2 h-4 w-4" /> <Eye className="mr-2 h-4 w-4" />
View Server Detail View Server Detail
</DropdownMenuItem> </DropdownMenuItem>
{server.docker === "true" && ( {server.docker === 'true' && (
<DropdownMenuItem <DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
onClick={(e) => {
e.stopPropagation();
handleViewContainers(server.id);
}}
>
<Activity className="mr-2 h-4 w-4" /> <Activity className="mr-2 h-4 w-4" />
Container Monitoring Container Monitoring
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
e.stopPropagation();
handlePauseResume(server);
}}
disabled={isProcessing} disabled={isProcessing}
> >
{isPaused ? ( {isPaused ? (
@@ -522,20 +370,12 @@ export const ServerTable = ({
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
onClick={(e) => {
e.stopPropagation();
handleEdit(server);
}}
>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Edit Server Edit Server
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
e.stopPropagation();
handleDelete(server);
}}
className="text-red-600 focus:text-red-600" className="text-red-600 focus:text-red-600"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
@@ -566,19 +406,19 @@ export const ServerTable = ({
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
Are you sure you want to delete this server?
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will permanently delete{" "} This action cannot be undone. This will permanently delete{' '}
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">
{selectedServer?.name} {selectedServer?.name}
</span>{" "} </span>{' '}
and all of its monitoring data. and all of its monitoring data.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel> <AlertDialogCancel disabled={isDeleting}>
Cancel
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={confirmDelete} onClick={confirmDelete}
disabled={isDeleting} disabled={isDeleting}
@@ -9,7 +9,6 @@ import {
import { ServiceForm } from "./ServiceForm"; import { ServiceForm } from "./ServiceForm";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useLanguage } from "@/contexts/LanguageContext";
interface AddServiceDialogProps { interface AddServiceDialogProps {
open: boolean; open: boolean;
@@ -17,7 +16,6 @@ interface AddServiceDialogProps {
} }
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) { export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
const { t } = useLanguage();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleSuccess = async () => { const handleSuccess = async () => {
// Immediately invalidate and refetch services data // Immediately invalidate and refetch services data
@@ -34,9 +32,9 @@ export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps)
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col"> <DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl">{t("createNewService")}</DialogTitle> <DialogTitle className="text-xl">Create New Service</DialogTitle>
<DialogDescription> <DialogDescription>
{t("createNewServiceDesc")} Fill in the details to create a new service to monitor.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}> <ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
@@ -2,7 +2,6 @@
import React from "react"; import React from "react";
import { Clock, TimerOff } from "lucide-react"; import { Clock, TimerOff } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useLanguage } from "@/contexts/LanguageContext";
interface LastCheckedTimeProps { interface LastCheckedTimeProps {
lastCheckedTime: string; lastCheckedTime: string;
@@ -11,7 +10,6 @@ interface LastCheckedTimeProps {
} }
export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => { export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => {
const { t } = useLanguage();
// Format the time without seconds to display a static time // Format the time without seconds to display a static time
const formatTimeWithoutSeconds = (timeString: string) => { const formatTimeWithoutSeconds = (timeString: string) => {
try { try {
@@ -57,7 +55,7 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
)} )}
<span> <span>
{isPaused ? t("pausedAt") : ""} {isPaused ? "Paused at " : ""}
{formattedTime} {formattedTime}
</span> </span>
</div> </div>
@@ -68,14 +66,14 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
> >
<div className="flex flex-col gap-1 text-xs"> <div className="flex flex-col gap-1 text-xs">
<div className="font-medium"> <div className="font-medium">
{isPaused ? t("monitoringPaused") : t("lastCheckDetails")} {isPaused ? "Monitoring Paused" : "Last Check Details"}
</div> </div>
<div> <div>
{isPaused ? t("noAutomaticChecks") : t("checkedAt") + `${formattedTime}`} {isPaused ? "No automatic checks" : `Checked at ${formattedTime}`}
</div> </div>
{interval && !isPaused && ( {interval && !isPaused && (
<div> <div>
{t("checkInterval")}: {formatInterval(interval)} Check interval: {formatInterval(interval)}
</div> </div>
)} )}
<div className="text-gray-400 text-[10px]"> <div className="text-gray-400 text-[10px]">
@@ -1,7 +1,5 @@
import {useLanguage} from "@/contexts/LanguageContext.tsx";
export function LoadingState() { export function LoadingState() {
const { t } = useLanguage();
return ( return (
<div className="flex items-center justify-center h-screen bg-background text-foreground"> <div className="flex items-center justify-center h-screen bg-background text-foreground">
<div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in"> <div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in">
@@ -9,8 +7,8 @@ export function LoadingState() {
<div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div> <div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div>
<div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div> <div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
</div> </div>
<h3 className="text-xl font-medium mb-1">{t("loadingServerData")}</h3> <h3 className="text-xl font-medium mb-1">Loading server data</h3>
<p className="text-muted-foreground">{t("retrievingYourInformation")}</p> <p className="text-muted-foreground">Please wait while we retrieve your information...</p>
</div> </div>
</div> </div>
); );
@@ -11,7 +11,6 @@ import { Service } from "@/types/service.types";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceEditDialogProps { interface ServiceEditDialogProps {
open: boolean; open: boolean;
@@ -20,7 +19,6 @@ interface ServiceEditDialogProps {
} }
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) { export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
const { t } = useLanguage();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -55,9 +53,9 @@ export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDi
}}> }}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col"> <DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl">{t("editService")}</DialogTitle> <DialogTitle className="text-xl">Edit Service</DialogTitle>
<DialogDescription> <DialogDescription>
{t("editServiceDesc")} Update the details of your monitored service.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{open && service && ( {open && service && (
@@ -4,7 +4,6 @@ import { Service, UptimeData } from "@/types/service.types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useMemo } from "react"; import { useMemo } from "react";
import { formatDistanceStrict } from "date-fns"; import { formatDistanceStrict } from "date-fns";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceStatsCardsProps { interface ServiceStatsCardsProps {
service: Service; service: Service;
@@ -12,8 +11,6 @@ interface ServiceStatsCardsProps {
} }
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) { export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
const { t } = useLanguage();
// Calculate uptime percentage // Calculate uptime percentage
const calculateUptimePercentage = () => { const calculateUptimePercentage = () => {
if (uptimeData.length === 0) return 100; if (uptimeData.length === 0) return 100;
@@ -112,46 +109,41 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md"> <Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">{t('responseTime')}</CardTitle> <CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">Response Time</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-blue-700 dark:text-white"> <div className="text-2xl font-bold text-blue-700 dark:text-white">
{service.responseTime}ms {service.responseTime}ms
</div> </div>
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('lastCheckedAt').replace('{datetime}', service.lastChecked)}</p> <p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Last checked at {service.lastChecked}</p>
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && ( {avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('avg')}: {avgResponseTime}ms ({t('lastUpChecksCount').replace('{count}', String(upChecks.length))})</p> <p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Avg: {avgResponseTime}ms (last {upChecks.length} up checks)</p>
)} )}
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md"> <Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">{t('uptime')}</CardTitle> <CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">Uptime</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-green-700 dark:text-white"> <div className="text-2xl font-bold text-green-700 dark:text-white">
{uptimePercentage}% {uptimePercentage}%
</div> </div>
<p className="text-xs text-green-600 dark:text-green-200 mt-1">{t('basedOnlastChecksCount').replace('{count}', String(uptimeData.length))}</p> <p className="text-xs text-green-600 dark:text-green-200 mt-1">Based on last {uptimeData.length} checks</p>
<div className="flex flex-col space-y-1 mt-2"> <div className="flex flex-col space-y-1 mt-2">
<div className="flex items-center text-xs text-green-600 dark:text-green-200"> <div className="flex items-center text-xs text-green-600 dark:text-green-200">
<ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" /> <ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" />
<span>{t("totalUptime")}: {uptimeStats.totalUptimeFormatted}</span> <span>Total uptime: {uptimeStats.totalUptimeFormatted}</span>
</div> </div>
<div className="flex items-center text-xs text-red-600 dark:text-red-200"> <div className="flex items-center text-xs text-red-600 dark:text-red-200">
<ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" /> <ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" />
<span>{t("totalDowntime")}: {uptimeStats.totalDowntimeFormatted}</span> <span>Total downtime: {uptimeStats.totalDowntimeFormatted}</span>
</div> </div>
{service.status !== "paused" && ( {service.status !== "paused" && (
<div className="flex items-center text-xs font-medium mt-1"> <div className="flex items-center text-xs font-medium mt-1">
<span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}> <span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
{ {service.status === "up" ? "Up" : "Down"} for {uptimeStats.currentStatusDuration}
service.status === "up" ?
t('upStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
:
t('downStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
}
</span> </span>
</div> </div>
)} )}
@@ -161,16 +153,16 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
<Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md"> <Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">{t('monitoringSettings')}</CardTitle> <CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">Monitoring Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200"> <div className="flex items-center text-sm text-purple-700 dark:text-purple-200">
<Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" /> <Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
<span>{t('monitoringSettingsInterval').replace('{interval}', String(service.interval))}</span> <span>Checked every {service.interval} seconds</span>
</div> </div>
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1"> <div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1">
<Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" /> <Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
<span>{service.type} {t('monitoringSettingsType')}</span> <span>{service.type} monitoring</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -81,8 +81,8 @@ export function ServicesPagination({
</Select> </Select>
<span className="text-sm text-muted-foreground whitespace-nowrap"> <span className="text-sm text-muted-foreground whitespace-nowrap">
{totalItems > 0 {totalItems > 0
? t("servicesPagination", {"startItem": startItem, "endItem": endItem, "totalItems": totalItems}) ? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
: t("servicesPaginationNoService") : `0 ${t("services") || "services"}`
} }
</span> </span>
</div> </div>
@@ -5,7 +5,6 @@ import { TooltipProvider } from '@/components/ui/tooltip';
import { UptimeStatusItem } from './uptime/UptimeStatusItem'; import { UptimeStatusItem } from './uptime/UptimeStatusItem';
import { uptimeService } from '@/services/uptimeService'; import { uptimeService } from '@/services/uptimeService';
import { UptimeData } from '@/types/service.types'; import { UptimeData } from '@/types/service.types';
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface UptimeBarProps { interface UptimeBarProps {
uptime: number; uptime: number;
@@ -16,7 +15,6 @@ interface UptimeBarProps {
} }
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => { const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
const { t } = useLanguage();
// Calculate date range for last 20 checks with much more aggressive caching // Calculate date range for last 20 checks with much more aggressive caching
const endDate = useMemo(() => new Date(), []); const endDate = useMemo(() => new Date(), []);
const startDate = useMemo(() => { const startDate = useMemo(() => {
@@ -25,21 +23,19 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
return start; return start;
}, [endDate, interval]); }, [endDate, interval]);
// Fetch uptime data with immediate display and background updates // Fetch uptime data with very aggressive caching to reduce API calls
const { data: uptimeData = [] } = useQuery({ const { data: uptimeData = [] } = useQuery({
queryKey: ['uptime-bar', serviceId, serviceType], queryKey: ['uptime-bar', serviceId, serviceType],
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType), queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
enabled: !!serviceId, enabled: !!serviceId,
staleTime: 300000, // Data stays fresh for 5 minutes - display immediately from cache staleTime: 30000, // Data is fresh for 30 seconds
gcTime: 900000, // Keep in cache for 15 minutes gcTime: 600000, // Keep in cache for 10 minutes
refetchInterval: 300000, // Reduced to 5 minute polling refetchInterval: 60000, // 1 minute polling
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
retry: 1, // Reduce retries for faster failure retry: 3,
retryDelay: 1000, // Faster retry delay retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
refetchIntervalInBackground: true, // Allow background updates
placeholderData: (previousData) => previousData, // Keep showing previous data while refetching
}); });
// Memoize the uptime calculation to prevent unnecessary recalculations // Memoize the uptime calculation to prevent unnecessary recalculations
@@ -88,7 +84,7 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
</div> </div>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{t('last20Checks')} Last 20 checks
</span> </span>
</TooltipProvider> </TooltipProvider>
); );
@@ -3,24 +3,22 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/comp
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types"; import { ServiceFormData } from "./types";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceBasicFieldsProps { interface ServiceBasicFieldsProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) { export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) {
const { t } = useLanguage();
return ( return (
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('serviceName')}</FormLabel> <FormLabel>Service Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={t('serviceNameDesc')} placeholder="Enter a descriptive name for your service"
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -6,14 +6,12 @@ import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types"; import { ServiceFormData } from "./types";
import { ServiceUrlField } from "./ServiceUrlField"; import { ServiceUrlField } from "./ServiceUrlField";
import { useState } from "react"; import { useState } from "react";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceConfigFieldsProps { interface ServiceConfigFieldsProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) { export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
const { t } = useLanguage();
const [isCustomInterval, setIsCustomInterval] = useState(false); const [isCustomInterval, setIsCustomInterval] = useState(false);
const intervalValue = form.watch("interval"); const intervalValue = form.watch("interval");
@@ -37,7 +35,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="interval" name="interval"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("checkInterval")}</FormLabel> <FormLabel>Check Interval</FormLabel>
{!isCustomInterval ? ( {!isCustomInterval ? (
<FormControl> <FormControl>
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}> <Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
@@ -45,13 +43,13 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="30">30 {t("seconds")}</SelectItem> <SelectItem value="30">30 seconds</SelectItem>
<SelectItem value="60">1 {t("minute")}</SelectItem> <SelectItem value="60">1 minute</SelectItem>
<SelectItem value="300">5 {t("minutes")}</SelectItem> <SelectItem value="300">5 minutes</SelectItem>
<SelectItem value="900">15 {t("minutes")}</SelectItem> <SelectItem value="900">15 minutes</SelectItem>
<SelectItem value="1800">30 {t("minutes")}</SelectItem> <SelectItem value="1800">30 minutes</SelectItem>
<SelectItem value="3600">1 {t("hour")}</SelectItem> <SelectItem value="3600">1 hour</SelectItem>
<SelectItem value="custom">{t("custom")}</SelectItem> <SelectItem value="custom">Custom</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@@ -60,7 +58,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<FormControl> <FormControl>
<Input <Input
type="number" type="number"
placeholder={t("checkIntervalPlaceholder")} placeholder="Enter interval in seconds"
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
min="10" min="10"
@@ -74,14 +72,14 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
}} }}
className="text-sm text-blue-600 hover:text-blue-800" className="text-sm text-blue-600 hover:text-blue-800"
> >
{t("backToPresets")} Back to presets
</button> </button>
</div> </div>
)} )}
<FormDescription className="text-xs"> <FormDescription className="text-xs">
{isCustomInterval {isCustomInterval
? t("checkIntervalDescCustom") ? "Enter custom interval in seconds (minimum 10 seconds)"
: t("checkIntervalDesc") : "How often to check the service status"
} }
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
@@ -94,22 +92,22 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="retries" name="retries"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("retryAttempts")}</FormLabel> <FormLabel>Retry Attempts</FormLabel>
<FormControl> <FormControl>
<Select onValueChange={field.onChange} value={field.value ?? "3"}> <Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="1">1 {t("attempt")}</SelectItem> <SelectItem value="1">1 attempt</SelectItem>
<SelectItem value="2">2 {t("attempts")}</SelectItem> <SelectItem value="2">2 attempts</SelectItem>
<SelectItem value="3">3 {t("attempts")}</SelectItem> <SelectItem value="3">3 attempts</SelectItem>
<SelectItem value="5">5 {t("attempts")}</SelectItem> <SelectItem value="5">5 attempts</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
<FormDescription className="text-xs"> <FormDescription className="text-xs">
{t("retryAttemptsDesc")} Number of retry attempts before marking as down
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useServiceSchema, ServiceFormData } from "./types"; import { serviceSchema, ServiceFormData } from "./types";
import { ServiceBasicFields } from "./ServiceBasicFields"; import { ServiceBasicFields } from "./ServiceBasicFields";
import { ServiceTypeField } from "./ServiceTypeField"; import { ServiceTypeField } from "./ServiceTypeField";
import { ServiceConfigFields } from "./ServiceConfigFields"; import { ServiceConfigFields } from "./ServiceConfigFields";
@@ -14,7 +14,6 @@ import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./ServiceRegionalFields"; import { ServiceRegionalFields } from "./ServiceRegionalFields";
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils"; import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceFormProps { interface ServiceFormProps {
onSuccess: () => void; onSuccess: () => void;
@@ -31,13 +30,10 @@ export function ServiceForm({
isEdit = false, isEdit = false,
onSubmitStart onSubmitStart
}: ServiceFormProps) { }: ServiceFormProps) {
const { t } = useLanguage();
const { toast } = useToast(); const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const serviceSchema = useServiceSchema();
// Initialize form with default values // Initialize form with default values
const form = useForm<ServiceFormData>({ const form = useForm<ServiceFormData>({
resolver: zodResolver(serviceSchema), resolver: zodResolver(serviceSchema),
@@ -128,23 +124,23 @@ export function ServiceForm({
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6"> <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('basicInformation')}</h3> <h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
<ServiceBasicFields form={form} /> <ServiceBasicFields form={form} />
<ServiceTypeField form={form} /> <ServiceTypeField form={form} />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('configuration')}</h3> <h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
<ServiceConfigFields form={form} /> <ServiceConfigFields form={form} />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('regionalMonitoring')}</h3> <h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
<ServiceRegionalFields form={form} /> <ServiceRegionalFields form={form} />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('notifications')}</h3> <h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
<ServiceNotificationFields form={form} /> <ServiceNotificationFields form={form} />
</div> </div>
</div> </div>
@@ -152,7 +148,7 @@ export function ServiceForm({
<ServiceFormActions <ServiceFormActions
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onCancel={onCancel} onCancel={onCancel}
submitLabel={isEdit ? t("updateService") : t("createService")} submitLabel={isEdit ? "Update Service" : "Create Service"}
/> />
</form> </form>
</Form> </Form>
@@ -2,7 +2,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { MouseEvent } from "react"; import { MouseEvent } from "react";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceFormActionsProps { interface ServiceFormActionsProps {
isSubmitting: boolean; isSubmitting: boolean;
@@ -15,8 +14,6 @@ export function ServiceFormActions({
onCancel, onCancel,
submitLabel = "Create Service" submitLabel = "Create Service"
}: ServiceFormActionsProps) { }: ServiceFormActionsProps) {
const {t} = useLanguage();
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => { const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
if (!isSubmitting) { if (!isSubmitting) {
@@ -32,7 +29,7 @@ export function ServiceFormActions({
variant="outline" variant="outline"
disabled={isSubmitting} disabled={isSubmitting}
> >
{t("cancel")} Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
@@ -41,7 +38,7 @@ export function ServiceFormActions({
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("processing")}... Processing...
</> </>
) : ( ) : (
submitLabel submitLabel
@@ -9,14 +9,12 @@ import { useQuery } from "@tanstack/react-query";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService"; import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceNotificationFieldsProps { interface ServiceNotificationFieldsProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) { export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) {
const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]); const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
// Get the current form values for debugging // Get the current form values for debugging
@@ -90,10 +88,10 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel className="text-base"> <FormLabel className="text-base">
{t("enableNotifications")} Enable Notifications
</FormLabel> </FormLabel>
<FormDescription> <FormDescription>
{t("enableNotificationsDesc")} Enable or disable notifications for this service
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -117,11 +115,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
name="notificationChannels" name="notificationChannels"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("notificationChannels")}</FormLabel> <FormLabel>Notification Channels</FormLabel>
<FormDescription> <FormDescription>
{notificationStatus === "enabled" {notificationStatus === "enabled"
? t("notificationChannelsEnabledDesc") ? "Select notification channels for this service"
: t("notificationChannelsDesc")} : "Enable notifications first to select channels"}
</FormDescription> </FormDescription>
{/* Display selected channels as badges */} {/* Display selected channels as badges */}
@@ -146,7 +144,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
value="" // Always reset to empty after selection value="" // Always reset to empty after selection
> >
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}> <SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder={t("notificationChannelsPlaceholder")} /> <SelectValue placeholder="Add a notification channel" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{alertConfigs {alertConfigs
@@ -171,7 +169,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
return ( return (
<FormItem> <FormItem>
<FormLabel>{t("alertTemplate")}</FormLabel> <FormLabel>Alert Template</FormLabel>
<FormControl> <FormControl>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
@@ -181,7 +179,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
disabled={notificationStatus !== "enabled" || isLoadingTemplates} disabled={notificationStatus !== "enabled" || isLoadingTemplates}
> >
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}> <SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder={isLoadingTemplates ? t("alertTemplateLoading") : t("alertTemplatePlaceholder")} /> <SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{serviceTemplates?.map((template) => ( {serviceTemplates?.map((template) => (
@@ -194,8 +192,8 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{notificationStatus === "enabled" {notificationStatus === "enabled"
? t("alertTemplateEnabledDesc") ? "Choose a template for alert messages"
: t("alertTemplateDesc")} : "Enable notifications first to select template"}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
); );
@@ -9,14 +9,12 @@ import { regionalService } from "@/services/regionalService";
import { MapPin, Loader2, X, Plus } from "lucide-react"; import { MapPin, Loader2, X, Plus } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceRegionalFieldsProps { interface ServiceRegionalFieldsProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) { export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
const { t } = useLanguage();
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled"); const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
const currentRegionalAgents = form.watch("regionalAgents") || []; const currentRegionalAgents = form.watch("regionalAgents") || [];
@@ -81,10 +79,10 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel className="text-base font-medium flex items-center gap-2"> <FormLabel className="text-base font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" /> <MapPin className="h-4 w-4" />
{t("regionalMonitoring")} Regional Monitoring
</FormLabel> </FormLabel>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{t("regionalMonitoringDesc")} Assign this service to regional monitoring agents for distributed monitoring
</div> </div>
</div> </div>
<FormControl> <FormControl>
@@ -109,7 +107,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
name="regionalAgents" name="regionalAgents"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("regionalAgents")}</FormLabel> <FormLabel>Regional Agents</FormLabel>
{/* Display selected agents */} {/* Display selected agents */}
{currentRegionalAgents.length > 0 && ( {currentRegionalAgents.length > 0 && (
@@ -142,12 +140,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={ <SelectValue placeholder={
isLoading isLoading
? t("regionalAgentsLoading") ? "Loading agents..."
: availableAgents.length > 0 : availableAgents.length > 0
? t("regionalAgentsAvailablePlaceholder") ? "Select additional regional agents..."
: currentRegionalAgents.length > 0 : currentRegionalAgents.length > 0
? t("regionalAgentsAllSelected") ? "All available agents selected"
: t("regionalAgentsNoAvailable") : "No regional agents available"
} /> } />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
@@ -156,14 +154,14 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<SelectItem value="loading" disabled> <SelectItem value="loading" disabled>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
t("regionalAgentsLoading") Loading agents...
</div> </div>
</SelectItem> </SelectItem>
) : availableAgents.length === 0 ? ( ) : availableAgents.length === 0 ? (
<SelectItem value="no-agents" disabled> <SelectItem value="no-agents" disabled>
{currentRegionalAgents.length > 0 {currentRegionalAgents.length > 0
? t("regionalAgentsAllSelected") ? "All available agents selected"
: t("regionalAgentsNoOnlineAvailable") : "No online regional agents available"
} }
</SelectItem> </SelectItem>
) : ( ) : (
@@ -192,13 +190,13 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && ( {regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
<p className="text-sm text-amber-600"> <p className="text-sm text-amber-600">
{t("regionalAgentsNotFoundMessage")} No online regional agents found. Services will use default monitoring.
</p> </p>
)} )}
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && ( {currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
<p className="text-sm text-orange-600"> <p className="text-sm text-orange-600">
{t("regionalAgentsNotSelectedMessage")} No regional agents selected. Service will use default monitoring.
</p> </p>
)} )}
@@ -4,14 +4,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Globe, Wifi, Server, Globe2 } from "lucide-react"; import { Globe, Wifi, Server, Globe2 } from "lucide-react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types"; import { ServiceFormData } from "./types";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceTypeFieldProps { interface ServiceTypeFieldProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceTypeField({ form }: ServiceTypeFieldProps) { export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
const { t } = useLanguage();
const getServiceIcon = (type: string) => { const getServiceIcon = (type: string) => {
switch (type) { switch (type) {
case "http": case "http":
@@ -33,7 +31,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
name="type" name="type"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("serviceType")}</FormLabel> <FormLabel>Service Type</FormLabel>
<FormControl> <FormControl>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
@@ -58,7 +56,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>HTTP/S</span> <span>HTTP/S</span>
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeHTTPDesc")} Monitor websites and REST APIs with HTTP/HTTPS Protocol
</p> </p>
</div> </div>
</SelectItem> </SelectItem>
@@ -69,7 +67,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>PING</span> <span>PING</span>
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{t("serviceTypePINGDesc")} Monitor host availability with PING Protocol
</p> </p>
</div> </div>
</SelectItem> </SelectItem>
@@ -80,7 +78,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>TCP</span> <span>TCP</span>
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeTCPDesc")} Monitor TCP port connectivity with TCP Protocol
</p> </p>
</div> </div>
</SelectItem> </SelectItem>
@@ -91,7 +89,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>DNS</span> <span>DNS</span>
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeDNSDesc")} Monitor DNS resolution
</p> </p>
</div> </div>
</SelectItem> </SelectItem>
@@ -3,14 +3,12 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescripti
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types"; import { ServiceFormData } from "./types";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceUrlFieldProps { interface ServiceUrlFieldProps {
form: UseFormReturn<ServiceFormData>; form: UseFormReturn<ServiceFormData>;
} }
export function ServiceUrlField({ form }: ServiceUrlFieldProps) { export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
const { t } = useLanguage();
const serviceType = form.watch("type"); const serviceType = form.watch("type");
const getPlaceholder = () => { const getPlaceholder = () => {
@@ -24,35 +22,35 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
case "dns": case "dns":
return "example.com"; return "example.com";
default: default:
return t("targetDefaultPlaceholder"); return "Enter URL or hostname";
} }
}; };
const getDescription = () => { const getDescription = () => {
switch (serviceType) { switch (serviceType) {
case "http": case "http":
return t("targetHTTPDesc"); return "Enter the full URL including protocol (http:// or https://)";
case "ping": case "ping":
return t("targetPINGDesc"); return "Enter hostname or IP address to ping";
case "tcp": case "tcp":
return t("targetTCPDesc"); return "Enter hostname or IP address for TCP connection test";
case "dns": case "dns":
return t("targetDNSDesc"); return "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)";
default: default:
return t("targetDefaultDesc"); return "Enter the target URL or hostname for monitoring";
} }
}; };
const getFieldLabel = () => { const getFieldLabel = () => {
switch (serviceType) { switch (serviceType) {
case "dns": case "dns":
return t("targetDNS"); return "Domain Name";
case "ping": case "ping":
return "Hostname/IP"; return "Hostname/IP";
case "tcp": case "tcp":
return "Hostname/IP"; return "Hostname/IP";
default: default:
return t("targetDefault"); return "Target URL/Host";
} }
}; };
@@ -101,7 +99,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
/> />
</FormControl> </FormControl>
<FormDescription className="text-xs"> <FormDescription className="text-xs">
{t("targetTCPPortDesc")} Enter the port number for TCP connection test
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -1,34 +1,13 @@
import { z } from "zod"; import { z } from "zod";
import type { ZodSchema } from "zod";
import { useLanguage } from "@/contexts/LanguageContext";
export const serviceSchema = z.object({
export type ServiceFormData = { name: z.string().min(1, "Service name is required"),
name: string;
type: "http" | "ping" | "tcp" | "dns";
url: string;
port?: string;
interval: string;
retries: string;
notificationStatus?: "enabled" | "disabled";
notificationChannels?: string[];
alertTemplate?: string;
regionalMonitoringEnabled?: boolean;
regionalAgents?: string[];
};
// Hook to use the schema with translations
export const useServiceSchema = () => {
const { t } = useLanguage();
return z.object({
name: z.string().min(1, t("serviceNameRequired")),
type: z.enum(["http", "ping", "tcp", "dns"]), type: z.enum(["http", "ping", "tcp", "dns"]),
url: z.string() url: z.string().min(1, "URL/Domain/Host is required").refine((value) => {
.min(1, t("urlDomainHostRequired")) // Basic validation - more specific validation can be added per type
.refine( return value.trim().length > 0;
(value) => value.trim().length > 0, }, "Please enter a valid URL, hostname, or domain"),
t("enterValidUrlHostnameDomain")
),
port: z.string().optional(), port: z.string().optional(),
interval: z.string(), interval: z.string(),
retries: z.string(), retries: z.string(),
@@ -39,4 +18,5 @@ export const useServiceSchema = () => {
regionalMonitoringEnabled: z.boolean().optional(), regionalMonitoringEnabled: z.boolean().optional(),
regionalAgents: z.array(z.string()).optional(), regionalAgents: z.array(z.string()).optional(),
}); });
};
export type ServiceFormData = z.infer<typeof serviceSchema>;
@@ -25,8 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead> <TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead> <TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead> <TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("errorMessage")}</TableHead> <TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Error Message</TableHead>
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("details")}</TableHead> <TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Details</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -9,12 +9,10 @@ import { IncidentTable } from "./IncidentTable";
import { StatusFilter, PageSize } from "./types"; import { StatusFilter, PageSize } from "./types";
import { getStatusChangeEvents } from "./utils"; import { getStatusChangeEvents } from "./utils";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) { export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) {
// Get current theme // Get current theme
const { theme } = useTheme(); const { theme } = useTheme();
const { t } = useLanguage();
// Filter state // Filter state
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all"); const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
@@ -67,7 +65,7 @@ export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] })
<CardHeader> <CardHeader>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<CardTitle className="text-card-foreground"> <CardTitle className="text-card-foreground">
<span>{t("incidentHistory")}</span> <span>Incident History</span>
</CardTitle> </CardTitle>
<StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} /> <StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} />
</div> </div>
@@ -15,7 +15,6 @@ import {
SelectValue SelectValue
} from "@/components/ui/select"; } from "@/components/ui/select";
import { PageSize } from "./types"; import { PageSize } from "./types";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface TablePaginationProps { interface TablePaginationProps {
currentPage: number; currentPage: number;
@@ -36,7 +35,6 @@ export function TablePagination({
onPageChange, onPageChange,
onPageSizeChange, onPageSizeChange,
}: TablePaginationProps) { }: TablePaginationProps) {
const {t} = useLanguage()
// Generate page numbers // Generate page numbers
const getPageNumbers = () => { const getPageNumbers = () => {
const pages = []; const pages = [];
@@ -60,7 +58,7 @@ export function TablePagination({
return ( return (
<div className="mt-4 flex items-center justify-between"> <div className="mt-4 flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">{t("rowsPerPage")}:</span> <span className="text-sm text-muted-foreground">Rows per page:</span>
<Select <Select
value={pageSize} value={pageSize}
onValueChange={(value) => onPageSizeChange(value as PageSize)} onValueChange={(value) => onPageSizeChange(value as PageSize)}
@@ -12,7 +12,6 @@ import {
import { Service } from "@/types/service.types"; import { Service } from "@/types/service.types";
import { serviceService } from "@/services/serviceService"; import { serviceService } from "@/services/serviceService";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceRowActionsProps { interface ServiceRowActionsProps {
service: Service; service: Service;
@@ -32,7 +31,6 @@ export const ServiceRowActions = ({
onMuteAlerts onMuteAlerts
}: ServiceRowActionsProps) => { }: ServiceRowActionsProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useLanguage();
// Handle pause/resume directly from dropdown // Handle pause/resume directly from dropdown
const handlePauseResume = async (e: React.MouseEvent) => { const handlePauseResume = async (e: React.MouseEvent) => {
@@ -124,7 +122,7 @@ export const ServiceRowActions = ({
}} }}
> >
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
<span>{t("viewDetail")}</span> <span>View Detail</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="flex items-center gap-2 cursor-pointer text-base py-2.5" className="flex items-center gap-2 cursor-pointer text-base py-2.5"
@@ -133,12 +131,12 @@ export const ServiceRowActions = ({
{service.status === "paused" ? ( {service.status === "paused" ? (
<> <>
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
<span>{t("resumeMonitoring")}</span> <span>Resume Monitoring</span>
</> </>
) : ( ) : (
<> <>
<Pause className="h-4 w-4" /> <Pause className="h-4 w-4" />
<span>{t("pauseMonitoring")}</span> <span>Pause Monitoring</span>
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
@@ -150,7 +148,7 @@ export const ServiceRowActions = ({
}} }}
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
<span>{t("edit")}</span> <span>Edit</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="flex items-center gap-2 cursor-pointer text-base py-2.5" className="flex items-center gap-2 cursor-pointer text-base py-2.5"
@@ -159,12 +157,12 @@ export const ServiceRowActions = ({
{alertsMuted ? ( {alertsMuted ? (
<> <>
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
<span>{t("unmuteAlerts")}</span> <span>Unmute Alerts</span>
</> </>
) : ( ) : (
<> <>
<BellOff className="h-4 w-4" /> <BellOff className="h-4 w-4" />
<span>{t("muteAlerts")}</span> <span>Mute Alerts</span>
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
@@ -177,7 +175,7 @@ export const ServiceRowActions = ({
}} }}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
<span>{t("delete")}</span> <span>Delete</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface UptimeSummaryProps { interface UptimeSummaryProps {
uptime: number; uptime: number;
@@ -8,14 +7,13 @@ interface UptimeSummaryProps {
} }
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => { export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
const { t } = useLanguage();
return ( return (
<div className="flex items-center justify-between text-xs mt-1"> <div className="flex items-center justify-between text-xs mt-1">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{Math.round(uptime)}% uptime {Math.round(uptime)}% uptime
</span> </span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{t('last20Checks')} Last 20 checks
</span> </span>
</div> </div>
); );
@@ -1,289 +1,46 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { format } from 'date-fns';
import {
Card, CardContent, CardDescription, CardHeader, CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, FolderOpen, Database, CheckCircle } from "lucide-react"; import {
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useSystemSettings } from "@/hooks/useSystemSettings"; import { useSystemSettings } from "@/hooks/useSystemSettings";
import { pb } from "@/lib/pocketbase";
import { toast } from "@/components/ui/use-toast";
export const AboutSystem: React.FC = () => { export const AboutSystem: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const { theme } = useTheme(); const { theme } = useTheme();
const { systemName } = useSystemSettings(); const { systemName } = useSystemSettings();
const [isImporting, setIsImporting] = useState(false);
const [mergeFields, setMergeFields] = useState(true);
const [importResult, setImportResult] = useState<{
success: boolean;
created: number;
updated: number;
skipped: number;
} | null>(null);
// Helper function to check if a collection is a system collection const [version, setVersion] = useState<string>('...');
const isSystemCollection = (name: string): boolean => { const [releaseDate, setReleaseDate] = useState<string>('...');
return name.startsWith('_') || ['users'].includes(name);
};
// Helper function to validate collection schema
const validateCollectionSchema = (collection: any): string | null => {
if (!collection.name) return "Collection name is required";
if (typeof collection.name !== 'string') return "Collection name must be a string";
if (!/^[a-z][a-z0-9_]*$/.test(collection.name)) {
return "Collection name must start with a letter and contain only lowercase letters, numbers, and underscores";
}
if (collection.name.length < 3 || collection.name.length > 32) {
return "Collection name must be between 3 and 32 characters";
}
// Validate schema array if present
if (collection.schema && !Array.isArray(collection.schema)) {
return "Collection schema must be an array";
}
// Validate each field in the schema
if (collection.schema) {
for (const field of collection.schema) {
if (!field.name || typeof field.name !== 'string') {
return `Invalid field name in collection '${collection.name}'`;
}
if (!field.type || typeof field.type !== 'string') {
return `Invalid field type for '${field.name}' in collection '${collection.name}'`;
}
}
}
return null;
};
const handleSchemaImport = async (schemaData: string) => {
if (!schemaData.trim()) {
toast({
title: "Error",
description: "No schema data provided",
variant: "destructive",
});
return;
}
useEffect(() => {
const fetchLatestRelease = async () => {
try { try {
setIsImporting(true); const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
setImportResult(null); const data = await res.json();
setVersion(data.tag_name || 'v1.x.x');
// Parse the JSON to validate it setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
const collections = JSON.parse(schemaData); } catch (err) {
setVersion('v1.x.x');
if (!Array.isArray(collections)) { setReleaseDate(t('unknown'));
throw new Error("Schema must be an array of collections");
}
// Check if user is authenticated as admin
if (!pb.authStore.isValid) {
throw new Error("Authentication required. Please log in as an admin.");
}
// Create/update collections one by one using PocketBase client
let successCount = 0;
let errorCount = 0;
let updatedCount = 0;
let skippedCount = 0;
const errors = [];
for (const collection of collections) {
try {
// Skip system collections
if (isSystemCollection(collection.name)) {
skippedCount++;
continue;
}
// Validate collection schema
const validationError = validateCollectionSchema(collection);
if (validationError) {
throw new Error(validationError);
}
// Check if collection already exists
const existingCollections = await pb.collections.getFullList();
const existingCollection = existingCollections.find(c => c.name === collection.name);
if (existingCollection) {
if (mergeFields) {
try {
// Get existing schema
const existingSchema = existingCollection.schema || [];
const newSchema = collection.schema || [];
// Merge schemas - add new fields, keep existing ones
const mergedSchema = [...existingSchema];
let fieldsAdded = 0;
for (const newField of newSchema) {
const existingFieldIndex = mergedSchema.findIndex(f => f.name === newField.name);
if (existingFieldIndex >= 0) {
} else {
// Add new field
mergedSchema.push(newField);
fieldsAdded++;
}
}
// Update the collection with merged schema
const updateData = {
...collection,
schema: mergedSchema
};
const updatedCollection = await pb.collections.update(existingCollection.id, updateData);
updatedCount++;
} catch (mergeError) {
throw new Error(`Failed to merge fields: ${mergeError.message || mergeError}`);
}
} else {
skippedCount++;
continue;
}
} else {
// Create new collection
try {
const newCollection = await pb.collections.create(collection);
successCount++;
} catch (createError) {
// Extract more detailed error information
let errorMessage = createError.message || createError;
if (createError.data) {
// Try to extract field-specific errors
if (createError.data.data) {
const fieldErrors = Object.entries(createError.data.data).map(([field, msgs]) =>
`${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`
).join('; ');
if (fieldErrors) {
errorMessage += ` - Field errors: ${fieldErrors}`;
}
}
}
throw new Error(`Failed to create collection: ${errorMessage}`);
}
}
} catch (error) {
errorCount++;
const errorMsg = `Failed to process collection '${collection.name}': ${error.message || error}`;
errors.push(errorMsg);
}
}
// Set import result for display
setImportResult({
success: successCount + updatedCount > 0,
created: successCount,
updated: updatedCount,
skipped: skippedCount
});
// Show results
const totalProcessed = successCount + updatedCount;
if (totalProcessed > 0 || skippedCount > 0) {
let description = '';
if (totalProcessed > 0) {
description = `Successfully processed ${totalProcessed} collection(s)`;
if (successCount > 0) description += ` (${successCount} created)`;
if (updatedCount > 0) description += ` (${updatedCount} updated)`;
}
if (skippedCount > 0) {
if (description) description += `, `;
description += `${skippedCount} skipped (system collections or existing)`;
}
if (errorCount > 0) description += `. ${errorCount} failed.`;
toast({
title: "Import Complete",
description,
variant: totalProcessed > errorCount ? "default" : "destructive",
});
} else if (errorCount > 0) {
toast({
title: "Import Failed",
description: "No collections were processed successfully.",
variant: "destructive",
});
} else {
toast({
title: "No Changes",
description: "All collections already exist and no changes were made.",
variant: "default",
});
}
if (errors.length > 0) {
// Show detailed errors for debugging
if (errors.length <= 3) {
toast({
title: "Detailed Errors",
description: errors.join('; '),
variant: "destructive",
});
}
}
} catch (error) {
toast({
title: "Import Failed",
description: error instanceof Error ? error.message : "Failed to import schema",
variant: "destructive",
});
setImportResult({
success: false,
created: 0,
updated: 0,
skipped: 0
});
} finally {
setIsImporting(false);
}
};
const handleLoadLocalSchema = async () => {
try {
setIsImporting(true);
setImportResult(null);
const response = await fetch('/upload/data/pb_schema_latest.json');
if (!response.ok) {
throw new Error(`Failed to load local schema: ${response.status} ${response.statusText}`);
}
const schemaText = await response.text();
// Validate that it's valid JSON
JSON.parse(schemaText);
// Directly import the schema
await handleSchemaImport(schemaText);
} catch (error) {
toast({
title: "Import Failed",
description: error instanceof Error ? error.message : "Failed to load and import local schema file",
variant: "destructive",
});
setImportResult({
success: false,
created: 0,
updated: 0,
skipped: 0
});
setIsImporting(false);
} }
}; };
fetchLatestRelease();
}, [t]);
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1> <h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
<p className="text-muted-foreground text-base leading-relaxed mt-2">
</p>
</div> </div>
<Separator /> <Separator />
@@ -301,7 +58,7 @@ export const AboutSystem: React.FC = () => {
<div className="flex flex-col space-y-3 pt-2"> <div className="flex flex-col space-y-3 pt-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('systemVersion')}</span> <span className="text-muted-foreground">{t('systemVersion')}</span>
<span className="text-foreground font-medium">{t('version')} 1.5.1</span> <span className="text-foreground font-medium">{version}</span>
</div> </div>
<Separator className="my-1" /> <Separator className="my-1" />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -311,7 +68,7 @@ export const AboutSystem: React.FC = () => {
<Separator className="my-1" /> <Separator className="my-1" />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('releasedOn')}</span> <span className="text-muted-foreground">{t('releasedOn')}</span>
<span className="text-foreground font-medium">Auguest 21, 2025</span> <span className="text-foreground font-medium">{releaseDate}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -324,7 +81,9 @@ export const AboutSystem: React.FC = () => {
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} /> <Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
<span>{t('links')}</span> <span>{t('links')}</span>
</CardTitle> </CardTitle>
<CardDescription className="font-medium text-base">{systemName || 'CheckCle'} {t('resources').toLowerCase()}</CardDescription> <CardDescription className="font-medium text-base">
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 pt-6"> <CardContent className="space-y-4 pt-6">
<div className="grid grid-cols-1 gap-3"> <div className="grid grid-cols-1 gap-3">
@@ -348,88 +107,6 @@ export const AboutSystem: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<Separator />
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
<CardTitle className="flex items-center gap-2">
<Database className={`h-5 w-5 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'}`} />
<span>{t('updateSchema')}</span>
</CardTitle>
<CardDescription className="font-medium text-base">{t('updateSchemaDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="merge-fields"
checked={mergeFields}
onCheckedChange={(checked) => setMergeFields(checked === true)}
/>
<Label htmlFor="merge-fields" className="text-sm font-medium">
{t('mergeFieldsLabel')}
</Label>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button
onClick={handleLoadLocalSchema}
disabled={isImporting}
className={`flex items-center gap-2 transition-all duration-200 ${
isImporting ? 'animate-pulse' : ''
}`}
>
<Database className={`h-4 w-4 ${isImporting ? 'animate-spin' : ''}`} />
{isImporting ? t('importing') : t('clickToUpdateSchema')}
</Button>
</div>
{importResult && (
<div className={`mt-4 p-4 rounded-lg border ${
importResult.success
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
} animate-in fade-in-0 slide-in-from-bottom-2 duration-300`}>
<div className="flex items-center gap-2">
<CheckCircle className={`h-5 w-5 ${
importResult.success
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`} />
<span className={`font-medium ${
importResult.success
? 'text-green-800 dark:text-green-200'
: 'text-red-800 dark:text-red-200'
}`}>
{importResult.success ? t('importSuccessful') : t('importFailed')}
</span>
</div>
<div className={`mt-2 text-sm ${
importResult.success
? 'text-green-700 dark:text-green-300'
: 'text-red-700 dark:text-red-300'
}`}>
{importResult.created > 0 && t('collectionsCreatedCount').replace('{count}', String(importResult.created))}
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + t('collectionsUpdatedCount').replace('{count}', String(importResult.updated))}
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + t('collectionsSkippedCount').replace('{count}', String(importResult.skipped))}
</div>
</div>
)}
<div className="text-sm text-muted-foreground">
<p className="mb-2">
<strong>{t('instructions')}:</strong>
</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>{t('mergeFields')}:</strong> {t('instructionsMergeFields')}</li>
<li>{t('instructionsCollections')}</li>
<li>{t('instructionsImportAuth')}</li>
</ul>
</div>
</CardContent>
</Card>
</div> </div>
); );
}; };
@@ -16,7 +16,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { TemplateType, templateTypeConfigs } from "@/services/templateService"; import { TemplateType, templateTypeConfigs } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {z} from "zod";
interface TemplateDialogProps { interface TemplateDialogProps {
open: boolean; open: boolean;
@@ -82,7 +81,6 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
}; };
const renderPlaceholderGuide = () => { const renderPlaceholderGuide = () => {
const config = templateTypeConfigs[selectedTemplateType]; const config = templateTypeConfigs[selectedTemplateType];
if (!config) return null; if (!config) return null;
@@ -52,6 +52,7 @@ const DataRetentionSettings = () => {
setLastCleanup(result.lastCleanup); setLastCleanup(result.lastCleanup);
} }
} catch (error) { } catch (error) {
console.error("Error loading retention settings:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to load retention settings", description: "Failed to load retention settings",
@@ -71,6 +72,7 @@ const DataRetentionSettings = () => {
description: "Data retention settings have been updated", description: "Data retention settings have been updated",
}); });
} catch (error) { } catch (error) {
console.error("Error saving retention settings:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to save retention settings", description: "Failed to save retention settings",
@@ -94,6 +96,7 @@ const DataRetentionSettings = () => {
// Reload settings to get updated last cleanup time // Reload settings to get updated last cleanup time
await loadSettings(); await loadSettings();
} catch (error) { } catch (error) {
console.error("Error during uptime cleanup:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to perform uptime data cleanup", description: "Failed to perform uptime data cleanup",
@@ -117,6 +120,7 @@ const DataRetentionSettings = () => {
// Reload settings to get updated last cleanup time // Reload settings to get updated last cleanup time
await loadSettings(); await loadSettings();
} catch (error) { } catch (error) {
console.error("Error during server cleanup:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to perform server data cleanup", description: "Failed to perform server data cleanup",
@@ -140,6 +144,7 @@ const DataRetentionSettings = () => {
// Reload settings to get updated last cleanup time // Reload settings to get updated last cleanup time
await loadSettings(); await loadSettings();
} catch (error) { } catch (error) {
console.error("Error during manual cleanup:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to perform database cleanup", description: "Failed to perform database cleanup",
@@ -205,16 +210,10 @@ const DataRetentionSettings = () => {
min="1" min="1"
max="365" max="365"
value={settings.uptimeRetentionDays} value={settings.uptimeRetentionDays}
onChange={(e) => { onChange={(e) => setSettings(prev => ({
const value = e.target.value;
if (value === '' || isNaN(Number(value))) {
return;
}
setSettings(prev => ({
...prev, ...prev,
uptimeRetentionDays: Number(value) uptimeRetentionDays: parseInt(e.target.value) || 30
})); }))}
}}
className="mt-1" className="mt-1"
/> />
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
@@ -230,16 +229,10 @@ const DataRetentionSettings = () => {
min="1" min="1"
max="365" max="365"
value={settings.serverRetentionDays} value={settings.serverRetentionDays}
onChange={(e) => { onChange={(e) => setSettings(prev => ({
const value = e.target.value;
if (value === '' || isNaN(Number(value))) {
return;
}
setSettings(prev => ({
...prev, ...prev,
serverRetentionDays: Number(value) serverRetentionDays: parseInt(e.target.value) || 30
})); }))}
}}
className="mt-1" className="mt-1"
/> />
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
@@ -257,7 +250,52 @@ const DataRetentionSettings = () => {
</Alert> </Alert>
)} )}
</CardContent> </CardContent>
<CardFooter className="flex justify-end"> <CardFooter className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2 w-full">
<Button
variant="outline"
onClick={handleUptimeShrink}
disabled={isUptimeShrinking}
className="flex items-center gap-2"
>
{isUptimeShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Globe className="h-4 w-4" />
)}
Shrink Uptime Data
</Button>
<Button
variant="outline"
onClick={handleServerShrink}
disabled={isServerShrinking}
className="flex items-center gap-2"
>
{isServerShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Server className="h-4 w-4" />
)}
Shrink Server Data
</Button>
<Button
variant="outline"
onClick={handleFullShrink}
disabled={isFullShrinking}
className="flex items-center gap-2"
>
{isFullShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
Full Database Shrink
</Button>
</div>
<div className="flex justify-end w-full">
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={isSaving} disabled={isSaving}
@@ -268,6 +306,7 @@ const DataRetentionSettings = () => {
) : null} ) : null}
Save Changes Save Changes
</Button> </Button>
</div>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
@@ -143,7 +143,7 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800"> <Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300"> <AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeAddUser")} <span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</CardContent> </CardContent>
@@ -153,11 +153,11 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
} }
if (isLoading) { if (isLoading) {
return <div className="p-4">{t("loadingSettings")}</div>; return <div className="p-4">Loading settings...</div>;
} }
if (error) { if (error) {
return <div className="p-4 text-red-500">{t("loadingSettingsError")}</div>; return <div className="p-4 text-red-500">Error loading settings</div>;
} }
return ( return (
@@ -126,6 +126,15 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
<Mail className="h-5 w-5" /> <Mail className="h-5 w-5" />
{t("sendTestEmail", "settings")} {t("sendTestEmail", "settings")}
</DialogTitle> </DialogTitle>
<Button
variant="ghost"
size="icon"
className="absolute right-4 top-4"
onClick={handleClose}
disabled={isLoading}
>
<X className="h-4 w-4" />
</Button>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
@@ -194,7 +203,9 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
{/* Info message */} {/* Info message */}
<Alert> <Alert>
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertDescription>{t("testEmailAlert", "settings")}</AlertDescription> <AlertDescription>
This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.
</AlertDescription>
</Alert> </Alert>
</div> </div>
@@ -212,7 +223,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
) : ( ) : (
<Mail className="h-4 w-4" /> <Mail className="h-4 w-4" />
)} )}
{isLoading ? t("sending", "settings") : t("send", "settings")} {isLoading ? t("sending", "settings") : t("send", "common")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -27,7 +27,6 @@ import { Switch } from "@/components/ui/switch";
import { Loader2, Copy } from "lucide-react"; import { Loader2, Copy } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
interface NotificationChannelDialogProps { interface NotificationChannelDialogProps {
open: boolean; open: boolean;
@@ -35,10 +34,9 @@ interface NotificationChannelDialogProps {
editingConfig: AlertConfiguration | null; editingConfig: AlertConfiguration | null;
} }
const baseSchema = z.object({ const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"), notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook"]), notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
service_id: z.string().default("global"), service_id: z.string().default("global"),
template_id: z.string().optional(), template_id: z.string().optional(),
@@ -80,36 +78,12 @@ const emailSchema = baseSchema.extend({
smtp_password: z.string().min(1, "SMTP password is required"), smtp_password: z.string().min(1, "SMTP password is required"),
}); });
const ntfySchema = baseSchema.extend({
notification_type: z.literal("ntfy"),
ntfy_endpoint: z.string().url("Must be a valid NTFY endpoint URL"),
api_token: z.string().optional(),
});
const pushoverSchema = baseSchema.extend({
notification_type: z.literal("pushover"),
api_token: z.string().min(1, "API token is required"),
user_key: z.string().min(1, "User key is required"),
});
const notifiarrSchema = baseSchema.extend({
notification_type: z.literal("notifiarr"),
api_token: z.string().min(1, "API token is required"),
channel_id: z.string().min(1, "Channel ID is required"),
});
const webhookSchema = baseSchema.extend({ const webhookSchema = baseSchema.extend({
notification_type: z.literal("webhook"), notification_type: z.literal("webhook"),
webhook_url: z.string().url("Must be a valid URL"), webhook_url: z.string().url("Must be a valid URL"),
webhook_payload_template: z.string().optional(), webhook_payload_template: z.string().optional(),
}); });
const gotifySchema = baseSchema.extend({
notification_type: z.literal("gotify"),
api_token: z.string().min(1, "API token is required"),
server_url: z.string().url("Must be a valid server URL"),
});
const formSchema = z.discriminatedUnion("notification_type", [ const formSchema = z.discriminatedUnion("notification_type", [
telegramSchema, telegramSchema,
discordSchema, discordSchema,
@@ -117,11 +91,7 @@ const formSchema = z.discriminatedUnion("notification_type", [
signalSchema, signalSchema,
googleChatSchema, googleChatSchema,
emailSchema, emailSchema,
ntfySchema, webhookSchema
pushoverSchema,
notifiarrSchema,
gotifySchema,
webhookSchema,
]); ]);
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@@ -163,31 +133,6 @@ const notificationTypeOptions = [
description: "Send notifications via email", description: "Send notifications via email",
icon: "/upload/notification/email.png" icon: "/upload/notification/email.png"
}, },
{
value: "ntfy",
label: "NTFY",
description: "Send notifications via NTFY push service",
icon: "/upload/notification/ntfy.png"
},
{
value: "pushover",
label: "Pushover",
description: "Send push notifications via Pushover",
icon: "/upload/notification/pushover.png"
},
{
value: "notifiarr",
label: "Notifiarr",
description: "Send notifications via Notifiarr",
icon: "/upload/notification/notifiarr.png"
},
{
value: "gotify",
label: "Gotify",
description: "Send push notifications via Gotify",
icon: "/upload/notification/gotify.png"
},
{ {
value: "webhook", value: "webhook",
label: "Webhook", label: "Webhook",
@@ -595,154 +540,6 @@ export const NotificationChannelDialog = ({
</> </>
)} )}
{notificationType === "ntfy" && (
<>
<FormField
control={form.control}
name="ntfy_endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>NTFY Endpoint</FormLabel>
<FormControl>
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
</FormControl>
<FormDescription>
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token (Optional)</FormLabel>
<FormControl>
<Input placeholder="Enter NTFY API token" {...field} type="password" />
</FormControl>
<FormDescription>
Optional API token for authentication with NTFY server
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "pushover" && (
<>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input placeholder="Your Pushover API token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Pushover application API token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_key"
render={({ field }) => (
<FormItem>
<FormLabel>User Key</FormLabel>
<FormControl>
<Input placeholder="Your Pushover user key" {...field} />
</FormControl>
<FormDescription>
Your Pushover user key (or group key)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "notifiarr" && (
<>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Notifiarr API token for sending notifications
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="channel_id"
render={({ field }) => (
<FormItem>
<FormLabel>Channel ID</FormLabel>
<FormControl>
<Input placeholder="Discord Channel ID" {...field} />
</FormControl>
<FormDescription>
The Discord channel ID where notifications will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "gotify" && (
<>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input placeholder="Your Gotify API token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Gotify application API token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="server_url"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormControl>
<Input placeholder="https://your-gotify-server.com" {...field} />
</FormControl>
<FormDescription>
The URL of your Gotify server
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "webhook" && ( {notificationType === "webhook" && (
<> <>
<FormField <FormField
@@ -67,8 +67,6 @@ export const NotificationChannelList = ({
case "signal": return "Signal"; case "signal": return "Signal";
case "google_chat": return "Google Chat"; case "google_chat": return "Google Chat";
case "email": return "Email"; case "email": return "Email";
case "pushover": return "Pushover";
case "notifiarr": return "Notifiarr";
case "webhook": return "Webhook"; case "webhook": return "Webhook";
default: return type || "Unknown"; default: return type || "Unknown";
} }
@@ -10,10 +10,7 @@ import { UserCog, Loader2, AlertCircle, Info, Users, ShieldAlert } from "lucide-
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { authService } from "@/services/authService"; import { authService } from "@/services/authService";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
const UserManagement = () => { const UserManagement = () => {
const { t } = useLanguage();
const { const {
users, users,
loading, loading,
@@ -47,21 +44,21 @@ const UserManagement = () => {
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full"> <AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
<div className="flex items-center"> <div className="flex items-center">
<UserCog className="h-5 w-5 mr-2 text-green-500" /> <UserCog className="h-5 w-5 mr-2 text-green-500" />
<span>{t("userManagement")}</span> <span>User Management</span>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg"> <AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">{t("userManagement")}</h2> <h2 className="text-2xl font-bold">User Management</h2>
{isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center"> {isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center">
<span className="mr-1">+</span> {t("addUser")} <span className="mr-1">+</span> Add User
</button>} </button>}
</div> </div>
{!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800"> {!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300"> <AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user. <span className="font-medium">Permission Notice:</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
</AlertDescription> </AlertDescription>
</Alert>} </Alert>}
@@ -92,8 +92,6 @@ export const SSLCertificatesTable = () => {
warning_threshold: updatedCertificate.warning_threshold, warning_threshold: updatedCertificate.warning_threshold,
expiry_threshold: updatedCertificate.expiry_threshold, expiry_threshold: updatedCertificate.expiry_threshold,
notification_channel: updatedCertificate.notification_channel, notification_channel: updatedCertificate.notification_channel,
notification_id: updatedCertificate.notification_id,
template_id: updatedCertificate.template_id,
check_interval: updatedCertificate.check_interval, check_interval: updatedCertificate.check_interval,
}); });
@@ -2,24 +2,13 @@ import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react"; import { Plus, RefreshCw } from "lucide-react";
import { import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner"; import { toast } from "sonner";
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards"; import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
import { SSLCertificatesTable } from "./SSLCertificatesTable"; import { SSLCertificatesTable } from "./SSLCertificatesTable";
import { LoadingState } from "@/components/services/LoadingState"; import { LoadingState } from "@/components/services/LoadingState";
import { import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
fetchSSLCertificates,
addSSLCertificate,
checkAndUpdateCertificate,
refreshAllCertificates,
deleteSSLCertificate,
} from "@/services/ssl";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm"; import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm"; import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types"; import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
@@ -32,23 +21,18 @@ export const SSLDomainContent = () => {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null); const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [isRefreshingAll, setIsRefreshingAll] = useState(false); const [isRefreshingAll, setIsRefreshingAll] = useState(false);
const [selectedCertificate, setSelectedCertificate] = const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
useState<SSLCertificate | null>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Fetch SSL certificates with explicit error handling // Fetch SSL certificates with explicit error handling
const { const { data: certificates = [], isLoading, error } = useQuery({
data: certificates = [], queryKey: ['ssl-certificates'],
isLoading,
error,
} = useQuery({
queryKey: ["ssl-certificates"],
queryFn: async () => { queryFn: async () => {
try { try {
const result = await fetchSSLCertificates(); const result = await fetchSSLCertificates();
return result; return result;
} catch (error) { } catch (error) {
toast.error(t("failedToLoadCertificates")); toast.error(t('failedToLoadCertificates'));
throw error; throw error;
} }
}, },
@@ -60,34 +44,32 @@ export const SSLDomainContent = () => {
const addMutation = useMutation({ const addMutation = useMutation({
mutationFn: addSSLCertificate, mutationFn: addSSLCertificate,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsAddDialogOpen(false); setIsAddDialogOpen(false);
toast.success(t("sslCertificateAdded")); toast.success(t('sslCertificateAdded'));
}, },
onError: (error) => { onError: (error) => {
toast.error( toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
error instanceof Error ? error.message : t("failedToAddCertificate") }
);
},
}); });
// Edit certificate mutation - Updated to include notification_id and template_id // Edit certificate mutation - Updated to include notification_id and template_id
const editMutation = useMutation({ const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => { mutationFn: async (certificate: SSLCertificate) => {
// Create the update data object with new fields // Create the update data object with new fields
const updateData = { const updateData = {
warning_threshold: Number(certificate.warning_threshold), warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold), expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel, notification_channel: certificate.notification_channel,
notification_id: certificate.notification_id || "", // Multi notification channels notification_id: certificate.notification_id || '', // Multi notification channels
template_id: certificate.template_id || "", // Alert template ID template_id: certificate.template_id || '', // Alert template ID
check_interval: certificate.check_interval, check_interval: certificate.check_interval,
}; };
// Update certificate in the database using PocketBase directly // Update certificate in the database using PocketBase directly
const updated = await pb const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
.collection("ssl_certificates")
.update(certificate.id, updateData);
// After updating the settings, refresh the certificate to ensure it's up to date // 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 // This will also check if notification needs to be sent based on updated thresholds
@@ -96,37 +78,33 @@ export const SSLDomainContent = () => {
return refreshedCert; return refreshedCert;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsEditDialogOpen(false); setIsEditDialogOpen(false);
setSelectedCertificate(null); setSelectedCertificate(null);
toast.success(t("sslCertificateUpdated")); toast.success(t('sslCertificateUpdated'));
}, },
onError: (error) => { onError: (error) => {
toast.error( toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
error instanceof Error ? error.message : t("failedToUpdateCertificate") }
);
},
}); });
// Delete certificate mutation // Delete certificate mutation
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: deleteSSLCertificate, mutationFn: deleteSSLCertificate,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
toast.success(t("sslCertificateDeleted")); toast.success(t('sslCertificateDeleted'));
}, },
onError: (error) => { onError: (error) => {
toast.error( toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
error instanceof Error ? error.message : t("failedToDeleteCertificate") }
);
},
}); });
// Refresh certificate mutation - Updated to remove individual toast notifications // Refresh certificate mutation - Updated to remove individual toast notifications
const refreshMutation = useMutation({ const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate, mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => { onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null); setRefreshingId(null);
// Removed individual success toast notification // Removed individual success toast notification
}, },
@@ -134,40 +112,33 @@ export const SSLDomainContent = () => {
setRefreshingId(null); setRefreshingId(null);
// Still refresh the data to show any partial information that was saved // Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
// Removed individual error toast notification // Removed individual error toast notification
}, }
}); });
// Refresh all certificates mutation // Refresh all certificates mutation
const refreshAllMutation = useMutation({ const refreshAllMutation = useMutation({
mutationFn: refreshAllCertificates, mutationFn: refreshAllCertificates,
onSuccess: (result) => { onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsRefreshingAll(false); setIsRefreshingAll(false);
if (result.failed === 0) { if (result.failed === 0) {
toast.success( toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
t("allCertificatesRefreshed").replace(
"{count}",
result.success.toString()
)
);
} else { } else {
toast.info( toast.info(t('someCertificatesFailed')
t("someCertificatesFailed") .replace('{success}', result.success.toString())
.replace("{success}", result.success.toString()) .replace('{failed}', result.failed.toString()));
.replace("{failed}", result.failed.toString())
);
} }
}, },
onError: (error) => { onError: (error) => {
toast.error(t("failedToCheckCertificate")); toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false); setIsRefreshingAll(false);
// Still refresh the data to show any partial information // Still refresh the data to show any partial information
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] }); queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}, }
}); });
const handleAddCertificate = async (data: AddSSLCertificateDto) => { const handleAddCertificate = async (data: AddSSLCertificateDto) => {
@@ -195,14 +166,12 @@ export const SSLDomainContent = () => {
const handleRefreshAll = async () => { const handleRefreshAll = async () => {
if (certificates.length === 0) { if (certificates.length === 0) {
toast.info(t("noCertificatesToRefresh")); toast.info(t('noCertificatesToRefresh'));
return; return;
} }
setIsRefreshingAll(true); setIsRefreshingAll(true);
toast.info( toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
t("startingRefreshAll").replace("{count}", certificates.length.toString())
);
refreshAllMutation.mutate(); refreshAllMutation.mutate();
}; };
@@ -213,29 +182,21 @@ export const SSLDomainContent = () => {
if (error) { if (error) {
return ( return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground"> <div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>{t("failedToLoadCertificates")}</p> <p>{t('failedToLoadCertificates')}</p>
<Button <Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
onClick={() => {t('check')}
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
}
>
{t("check")}
</Button> </Button>
</div> </div>
); );
} }
return ( return (
<main className="flex-1 flex flex-col overflow-auto bg-background pb-0 dark:bg-black p-2 "> <main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-6 rounded-xl"> <div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<div> <div>
<h2 className="text-2xl font-bold text-foreground"> <h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
{t("sslDomainManagement")} <p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t("monitorSSLCertificates")}
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
@@ -244,12 +205,8 @@ export const SSLDomainContent = () => {
disabled={isRefreshingAll || refreshingId !== null} disabled={isRefreshingAll || refreshingId !== null}
className="relative" className="relative"
> >
<RefreshCw <RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
className={`w-4 h-4 mr-2 ${ {t('refreshAll')}
isRefreshingAll ? "animate-spin" : ""
}`}
/>
{t("refreshAll")}
{isRefreshingAll && ( {isRefreshingAll && (
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"> <span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
... ...
@@ -260,7 +217,7 @@ export const SSLDomainContent = () => {
className="text-primary-foreground" className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)} onClick={() => setIsAddDialogOpen(true)}
> >
<Plus className="w-4 h-4 mr-2" /> {t("addDomain")} <Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -275,7 +232,7 @@ export const SSLDomainContent = () => {
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t("addSSLCertificate")}</DialogTitle> <DialogTitle>{t('addSSLCertificate')}</DialogTitle>
</DialogHeader> </DialogHeader>
<AddSSLCertificateForm <AddSSLCertificateForm
onSubmit={handleAddCertificate} onSubmit={handleAddCertificate}
@@ -286,16 +243,13 @@ export const SSLDomainContent = () => {
</Dialog> </Dialog>
{selectedCertificate && ( {selectedCertificate && (
<Dialog <Dialog open={isEditDialogOpen} onOpenChange={(open) => {
open={isEditDialogOpen}
onOpenChange={(open) => {
setIsEditDialogOpen(open); setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null); if (!open) setSelectedCertificate(null);
}} }}>
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t("editSSLCertificate")}</DialogTitle> <DialogTitle>{t('editSSLCertificate')}</DialogTitle>
</DialogHeader> </DialogHeader>
<EditSSLCertificateForm <EditSSLCertificateForm
certificate={selectedCertificate} certificate={selectedCertificate}
@@ -1,14 +0,0 @@
// src/contexts/FontSizeContext.js
import { useEffect } from "react";
const useDynamicFontSize = (size) => {
useEffect(() => {
if (typeof size === "number") {
// Apply font size to the root HTML element
document.documentElement.style.fontSize = `${size}px`;
localStorage.setItem("fontSize", size.toString());
}
}, [size]);
};
export default useDynamicFontSize;
+14 -75
View File
@@ -1,22 +1,16 @@
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react"; import React, { createContext, useContext, useState, ReactNode } from "react";
import { translations, Language, TranslationModule, TranslationKey } from "@/translations"; import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
type TranslationVars = Record<string, string | number>;
type LanguageContextType = { type LanguageContextType = {
language: Language; language: Language;
setLanguage: (language: Language) => void; setLanguage: (language: Language) => void;
t: { t: <M extends TranslationModule>(key: string, module?: M) => string;
(key: string): string;
(key: string, vars: TranslationVars): string;
<M extends TranslationModule>(key: string, module: M): string;
<M extends TranslationModule>(key: string, module: M, vars: TranslationVars): string;
};
}; };
// ❗ Create the context with `undefined` to enforce provider usage
const LanguageContext = createContext<LanguageContextType | undefined>(undefined); const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
// eslint-disable-next-line react-refresh/only-export-components // ✅ Stable custom hook
export const useLanguage = () => { export const useLanguage = () => {
const context = useContext(LanguageContext); const context = useContext(LanguageContext);
if (!context) { if (!context) {
@@ -27,78 +21,23 @@ export const useLanguage = () => {
export const LanguageProvider = ({ children }: { children: ReactNode }) => { export const LanguageProvider = ({ children }: { children: ReactNode }) => {
const [language, setLanguage] = useState<Language>("en"); const [language, setLanguage] = useState<Language>("en");
const fallbackLanguage: Language = "en";
const interpolate = (template: string, vars: TranslationVars): string => {
return template.replace(/{(\w+)}/g, (match, key) => {
return vars[key] !== undefined ? String(vars[key]) : match;
});
};
const t = useCallback(((
key: string,
moduleOrVars?: TranslationModule | TranslationVars,
vars?: TranslationVars
): string => {
let module: TranslationModule | undefined;
let variables: TranslationVars | undefined;
if (typeof moduleOrVars === "string") {
module = moduleOrVars;
variables = vars;
} else if (typeof moduleOrVars === "object") {
variables = moduleOrVars;
}
const langPack = translations[language];
const fallbackPack = translations[fallbackLanguage];
let translatedText = "";
const t = <M extends TranslationModule>(key: string, module?: M): string => {
if (module) { if (module) {
const moduleKey = key as TranslationKey<typeof module>; const translatedValue = translations[language][module][key as TranslationKey<M>];
const valCur = langPack?.[module]?.[moduleKey]; return typeof translatedValue === "string" ? translatedValue : key;
if (typeof valCur === "string") {
translatedText = valCur;
} else {
const valEn = fallbackPack?.[module]?.[moduleKey];
translatedText = typeof valEn === "string" ? valEn : key;
} }
} else {
let found = false;
for (const mod of Object.keys(langPack) as TranslationModule[]) { for (const mod in translations[language]) {
const moduleTranslations = langPack[mod]; const moduleKey = mod as TranslationModule;
if (moduleTranslations && key in moduleTranslations) { const translatedValue = translations[language][moduleKey][key as any];
const val = moduleTranslations[key as keyof typeof moduleTranslations]; if (translatedValue && typeof translatedValue === "string") {
if (typeof val === "string") { return translatedValue;
translatedText = val;
found = true;
break;
}
} }
} }
if (!found) { return key;
for (const mod of Object.keys(fallbackPack) as TranslationModule[]) { };
const moduleTranslations = fallbackPack[mod];
if (moduleTranslations && key in moduleTranslations) {
const val = moduleTranslations[key as keyof typeof moduleTranslations];
if (typeof val === "string") {
translatedText = val;
found = true;
break;
}
}
}
}
if (!found) {
translatedText = key;
}
}
return variables ? interpolate(translatedText, variables) : translatedText;
}) as LanguageContextType['t'], [language]);
return ( return (
<LanguageContext.Provider value={{ language, setLanguage, t }}> <LanguageContext.Provider value={{ language, setLanguage, t }}>
+14 -24
View File
@@ -1,48 +1,38 @@
import React, { createContext, useContext, useState, useEffect } from "react";
type SideBar = boolean; import React, { createContext, useContext, useState } from 'react';
interface SidebarContextType { interface SidebarContextType {
sidebarCollapsed: SideBar; sidebarCollapsed: boolean;
setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>; setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebar: () => void; toggleSidebar: () => void;
} }
const SidebarContext = createContext<SidebarContextType | undefined>(undefined); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
interface SidebarProviderProps {
children: React.ReactNode; export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
} const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
export const SidebarProvider: React.FC<SidebarProviderProps> = ({
children,
}) => {
// Initialize the state
const [sidebarCollapsed, setSidebarCollapsed] = useState<SideBar>(() => {
const saved = localStorage.getItem("sidebarCollapsed");
return saved === "true";
});
const toggleSidebar = () => { const toggleSidebar = () => {
setSidebarCollapsed((prev) => { setSidebarCollapsed(prev => !prev);
const newState = !prev;
localStorage.setItem("sidebarCollapsed", newState.toString());
return newState;
});
}; };
const value = { const value = {
sidebarCollapsed, sidebarCollapsed,
setSidebarCollapsed, setSidebarCollapsed,
toggleSidebar, toggleSidebar
}; };
return ( return (
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider> <SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
); );
}; };
// A custom hook to consume the sidebar context.
export const useSidebar = () => { export const useSidebar = () => {
const context = useContext(SidebarContext); const context = useContext(SidebarContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useSidebar must be used within a SidebarProvider"); throw new Error('useSidebar must be used within a SidebarProvider');
} }
return context; return context;
}; };
+15 -29
View File
@@ -1,3 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
@@ -25,21 +26,16 @@ const InstanceMonitoring = () => {
total: 0, total: 0,
online: 0, online: 0,
offline: 0, offline: 0,
warning: 0, warning: 0
}); });
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const { const { data: servers = [], isLoading, error, refetch } = useQuery({
data: servers = [], queryKey: ['servers'],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["servers"],
queryFn: serverService.getServers, queryFn: serverService.getServers,
refetchInterval: 30000, // Refetch every 30 seconds refetchInterval: 30000 // Refetch every 30 seconds
}); });
useEffect(() => { useEffect(() => {
@@ -54,7 +50,7 @@ const InstanceMonitoring = () => {
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
navigate("/login"); navigate('/login');
}; };
const handleAgentAdded = () => { const handleAgentAdded = () => {
@@ -74,12 +70,9 @@ const InstanceMonitoring = () => {
/> />
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6"> <main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
<div className="text-center max-w-md w-full"> <div className="text-center max-w-md w-full">
<h2 className="text-xl sm:text-2xl font-bold mb-4"> <h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2>
Error loading servers
</h2>
<p className="text-muted-foreground mb-4 text-sm sm:text-base"> <p className="text-muted-foreground mb-4 text-sm sm:text-base">
Unable to fetch server data. Please check your connection and Unable to fetch server data. Please check your connection and try again.
try again.
</p> </p>
<button <button
onClick={handleRefresh} onClick={handleRefresh}
@@ -104,11 +97,11 @@ const InstanceMonitoring = () => {
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<main className="flex-1 overflow-auto dark:bg-black p-2 shadow"> <main className="flex-1 overflow-auto">
<div className="p-4 sm:p-2 lg:p-2 space-y-6 h-full bg-white dark:bg-black p-4 rounded-xl"> <div className="p-4 sm:p-6 lg:p-8 space-y-6">
{/* Header Section */} {/* Header Section */}
<div> <div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-2 "> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h1 className="text-2xl lg:text-2xl font-bold text-foreground"> <h1 className="text-2xl lg:text-2xl font-bold text-foreground">
Instance Monitoring Instance Monitoring
@@ -117,10 +110,7 @@ const InstanceMonitoring = () => {
Monitor and manage your server instances in real-time Monitor and manage your server instances in real-time
</p> </p>
</div> </div>
<Button <Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
onClick={() => setAddDialogOpen(true)}
className="flex-shrink-0"
>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add Server Agent Add Server Agent
</Button> </Button>
@@ -128,17 +118,13 @@ const InstanceMonitoring = () => {
</div> </div>
{/* Stats Cards Section */} {/* Stats Cards Section */}
<div className="p-2 "> <div>
<ServerStatsCards stats={stats} /> <ServerStatsCards stats={stats} />
</div> </div>
{/* Server Table Section */} {/* Server Table Section */}
<div className="p-3"> <div>
<ServerTable <ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
servers={servers}
isLoading={isLoading}
onRefresh={handleRefresh}
/>
</div> </div>
</div> </div>
</main> </main>
+4 -7
View File
@@ -10,11 +10,8 @@ import { User } from "@/services/userService";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useSidebar } from "@/contexts/SidebarContext"; import { useSidebar } from "@/contexts/SidebarContext";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
const Profile = () => { const Profile = () => {
const { t } = useLanguage()
// Use shared sidebar state // Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar(); const { sidebarCollapsed, toggleSidebar } = useSidebar();
@@ -87,12 +84,12 @@ const Profile = () => {
return ( return (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
<div className="text-center"> <div className="text-center">
<p>{t("loginToViewProfile")}</p> <p>Please login to view your profile</p>
<button <button
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="mt-4 px-4 py-2 bg-primary text-white rounded" className="mt-4 px-4 py-2 bg-primary text-white rounded"
> >
{t("goToLogin")} Go to Login
</button> </button>
</div> </div>
</div> </div>
@@ -113,7 +110,7 @@ const Profile = () => {
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-12 w-12 animate-spin text-primary" /> <Loader2 className="h-12 w-12 animate-spin text-primary" />
<span className="ml-2">{t("loadingUserData")}</span> <span className="ml-2">Loading user data...</span>
</div> </div>
) : error ? ( ) : error ? (
<div className="flex flex-col items-center justify-center h-full"> <div className="flex flex-col items-center justify-center h-full">
@@ -124,7 +121,7 @@ const Profile = () => {
onClick={() => fetchUserData()} onClick={() => fetchUserData()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md" className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
> >
{t("retry")} Retry
</button> </button>
</div> </div>
) : ( ) : (
+7 -17
View File
@@ -1,3 +1,4 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { authService } from "@/services/authService"; import { authService } from "@/services/authService";
@@ -6,11 +7,7 @@ import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar"; import { Sidebar } from "@/components/dashboard/Sidebar";
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent"; import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
import { LoadingState } from "@/components/services/LoadingState"; import { LoadingState } from "@/components/services/LoadingState";
import { import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
fetchSSLCertificates,
shouldRunDailyCheck,
checkAllCertificatesAndNotify,
} from "@/services/sslCertificateService";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { useSidebar } from "@/contexts/SidebarContext"; import { useSidebar } from "@/contexts/SidebarContext";
@@ -26,13 +23,8 @@ const SslDomain = () => {
const navigate = useNavigate(); const navigate = useNavigate();
// Fetch SSL certificates with error handling and debugging // Fetch SSL certificates with error handling and debugging
const { const { data: certificates = [], isLoading, error, refetch } = useQuery({
data: certificates = [], queryKey: ['ssl-certificates'],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["ssl-certificates"],
queryFn: async () => { queryFn: async () => {
// console.log("Fetching SSL certificates from SslDomain page..."); // console.log("Fetching SSL certificates from SslDomain page...");
try { try {
@@ -102,17 +94,15 @@ const SslDomain = () => {
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<div className="flex flex-col items-center justify-center h-full p-6"> <div className="flex flex-col items-center justify-center h-full p-6">
<h2 className="text-xl font-bold mb-2"> <h2 className="text-xl font-bold mb-2">{t('failedToLoadCertificates')}</h2>
{t("failedToLoadCertificates")}
</h2>
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
{error instanceof Error ? error.message : t("unknown")} {error instanceof Error ? error.message : t('unknown')}
</p> </p>
<button <button
className="px-4 py-2 bg-primary text-primary-foreground rounded-md" className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
onClick={() => refetch()} onClick={() => refetch()}
> >
{t("check")} {t('check')}
</button> </button>
</div> </div>
</div> </div>
+3 -21
View File
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
collectionId?: string; collectionId?: string;
collectionName?: string; collectionName?: string;
service_id: string; service_id: string;
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook"; notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
telegram_chat_id?: string; telegram_chat_id?: string;
discord_webhook_url?: string; discord_webhook_url?: string;
signal_number?: string; signal_number?: string;
@@ -28,10 +28,7 @@ export interface AlertConfiguration {
smtp_password?: string; smtp_password?: string;
webhook_id?: string; webhook_id?: string;
channel_id?: string; channel_id?: string;
ntfy_endpoint?: string; // Webhook fields for alert_configurations
api_token?: string;
user_key?: string;
server_url?: string;
webhook_url?: string; webhook_url?: string;
webhook_payload_template?: string; webhook_payload_template?: string;
} }
@@ -86,23 +83,8 @@ export const alertConfigService = {
cleanConfig.smtp_port = config.smtp_port || ""; cleanConfig.smtp_port = config.smtp_port || "";
cleanConfig.smtp_password = config.smtp_password || ""; cleanConfig.smtp_password = config.smtp_password || "";
} else if (config.notification_type === "ntfy") {
cleanConfig.ntfy_endpoint = config.ntfy_endpoint || "";
cleanConfig.api_token = config.api_token || "";
} else if (config.notification_type === "pushover") {
cleanConfig.api_token = config.api_token || "";
cleanConfig.user_key = config.user_key || "";
} else if (config.notification_type === "notifiarr") {
cleanConfig.api_token = config.api_token || "";
cleanConfig.channel_id = config.channel_id || "";
} else if (config.notification_type === "gotify") {
cleanConfig.api_token = config.api_token || "";
cleanConfig.server_url = config.server_url || "";
} else if (config.notification_type === "webhook") { } else if (config.notification_type === "webhook") {
cleanConfig.webhook_url = config.webhook_url || ""; cleanConfig.webhook_url = config.webhook_url || "";
cleanConfig.webhook_payload_template = config.webhook_payload_template || ""; cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
@@ -56,9 +56,7 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
affected_systems: data.affected_systems, affected_systems: data.affected_systems,
priority: data.priority.toLowerCase(), priority: data.priority.toLowerCase(),
service_id: data.service_id || '', service_id: data.service_id || '',
//assigned_to: data.assigned_to || '', // Direct user ID assignment assigned_to: data.assigned_to || '', // Direct user ID assignment
//2025-08-18: // map UI field -> server schema
assigned_users: typeof data.assigned_to === 'string' ? data.assigned_to : '',
root_cause: data.root_cause || '', root_cause: data.root_cause || '',
resolution_steps: data.resolution_steps || '', resolution_steps: data.resolution_steps || '',
lessons_learned: data.lessons_learned || '', lessons_learned: data.lessons_learned || '',
@@ -85,27 +83,16 @@ export const updateIncident = async (id: string, data: Partial<IncidentItem>): P
try { try {
console.log(`Updating incident ${id} with:`, data); console.log(`Updating incident ${id} with:`, data);
const payload: Record<string, any> = { // Make sure impact and priority are lowercase
const payload = {
...data, ...data,
...(data.impact ? { impact: data.impact.toLowerCase() } : {}), impact: data.impact?.toLowerCase(),
...(data.priority ? { priority: data.priority.toLowerCase() } : {}), priority: data.priority?.toLowerCase(),
...(data.status status: data.status ? formatStatus(data.status) : undefined,
? { impact_status: data.status ? data.status.toLowerCase() : undefined,
status: formatStatus(data.status), ...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
impact_status: data.status.toLowerCase(),
}
: {}),
// set only if assigned_to was provided
...(typeof data.assigned_to === 'string'
? {
assigned_users: data.assigned_to, // server field
assigned_to: data.assigned_to, // legacy field for backward compatibility
}
: {}),
// add resolution_time only if status changes to resolved
...((data.status?.toLowerCase() === 'resolved' && !data.resolution_time)
? { resolution_time: new Date().toISOString() } ? { resolution_time: new Date().toISOString() }
: {}), : {})
}; };
console.log("Final payload for update:", payload); console.log("Final payload for update:", payload);
@@ -120,5 +107,3 @@ export const updateIncident = async (id: string, data: Partial<IncidentItem>): P
throw error; throw error;
} }
}; };
@@ -1,7 +1,6 @@
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import { IncidentItem } from '../types'; import { IncidentItem } from '../types';
import { userService } from '@/services/userService';
import { import {
addBasicInfoSection, addBasicInfoSection,
addDescriptionSection, addDescriptionSection,
@@ -23,18 +22,6 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
throw new Error('Invalid incident data'); throw new Error('Invalid incident data');
} }
// Fetch assigned user data if available
let assignedUser: { full_name?: string; username?: string } | null = null;
const assigneeId = incident?.assigned_users || incident?.assigned_to;
if (assigneeId) {
try {
assignedUser = await userService.getUser(assigneeId);
} catch (error) {
console.warn('Failed to fetch assigned user for PDF:', error);
// Continue without user data
}
}
try { try {
// Create new PDF document with portrait orientation // Create new PDF document with portrait orientation
const doc = new jsPDF({ const doc = new jsPDF({
@@ -98,7 +85,7 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
} }
// Add assignment section // Add assignment section
yPos = addAssignmentSection(doc, incident, yPos, assignedUser); yPos = addAssignmentSection(doc, incident, yPos);
// Check if we need to add a new page // Check if we need to add a new page
if (yPos > 250) { if (yPos > 250) {
@@ -146,7 +146,7 @@ export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: n
/** /**
* Add the assignment information section to the PDF * Add the assignment information section to the PDF
*/ */
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number, assignedUser?: { full_name?: string; username?: string } | null): number => { export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
doc.setFontSize(14); doc.setFontSize(14);
doc.setTextColor(30, 64, 175); // Blue-800 doc.setTextColor(30, 64, 175); // Blue-800
doc.text('Assignment Information', 15, yPos); doc.text('Assignment Information', 15, yPos);
@@ -158,16 +158,8 @@ export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: n
yPos += 10; yPos += 10;
doc.setFontSize(10); doc.setFontSize(10);
doc.setTextColor(0, 0, 0); doc.setTextColor(0, 0, 0);
const assignedTo = incident.assigned_to || 'Not assigned';
if (assignedUser) {
// Show user name if available
const userName = assignedUser.full_name || assignedUser.username || 'Unknown User';
doc.text(`Assigned to: ${userName}`, 15, yPos);
} else {
// Fallback to ID if no user data available
const assignedTo = incident.assigned_users || incident.assigned_to || 'Not assigned';
doc.text(`Assigned to: ${assignedTo}`, 15, yPos); doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
}
return yPos + 10; return yPos + 10;
}; };
+3 -6
View File
@@ -5,8 +5,7 @@ export type IncidentItem = {
service_id?: string; service_id?: string;
timestamp?: string; timestamp?: string;
description: string; description: string;
assigned_to?: string; // legacy UI field assigned_to?: string;
assigned_users?: string; // server field
resolution_time?: string; resolution_time?: string;
impact: string; impact: string;
affected_systems: string; affected_systems: string;
@@ -33,8 +32,7 @@ export type CreateIncidentInput = {
affected_systems: string; affected_systems: string;
priority: string; priority: string;
service_id?: string; service_id?: string;
assigned_to?: string; // legacy UI field assigned_to?: string;
assigned_users?: string; // server field
root_cause?: string; root_cause?: string;
resolution_steps?: string; resolution_steps?: string;
lessons_learned?: string; lessons_learned?: string;
@@ -52,8 +50,7 @@ export type UpdateIncidentInput = {
affected_systems?: string; affected_systems?: string;
priority?: string; priority?: string;
service_id?: string; service_id?: string;
assigned_to?: string; // legacy UI field assigned_to?: string;
assigned_users?: string; // server field
root_cause?: string; root_cause?: string;
resolution_steps?: string; resolution_steps?: string;
lessons_learned?: string; lessons_learned?: string;
+1 -2
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about'; import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = { export const aboutTranslations: AboutTranslations = {
aboutCheckCle: "Über Checkcle", aboutCheckcle: "Über Checkcle",
systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.", systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.",
systemVersion: "Systemversion", systemVersion: "Systemversion",
license: "Lizenz", license: "Lizenz",
@@ -15,7 +15,6 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "Schnelle Aktionen", quickActions: "Schnelle Aktionen",
quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.", quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.",
quickTips: "Schnelle Tipps", quickTips: "Schnelle Tipps",
releasedOn: "Veröffentlicht",
}; };
+2 -4
View File
@@ -6,11 +6,9 @@ export const commonTranslations: CommonTranslations = {
logout: "Abmelden", logout: "Abmelden",
language: "Sprache", language: "Sprache",
english: "Englisch", english: "Englisch",
khmer: "ភាសាខ្មែរ", khmer: "Khmer",
german: "Deutsch", german: "Deutsch",
korean: "한국어", simplifiedChinese: "Simplified Chinese",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "Guten Morgen", goodMorning: "Guten Morgen",
goodAfternoon: "Guten Nachmittag", goodAfternoon: "Guten Nachmittag",
goodEvening: "Guten Abend", goodEvening: "Guten Abend",
@@ -51,13 +51,4 @@ export const incidentTranslations: IncidentTranslations = {
configuration: 'Konfiguration', configuration: 'Konfiguration',
failedToUpdateStatus: 'Fehler beim Aktualisieren des Status', failedToUpdateStatus: 'Fehler beim Aktualisieren des Status',
inProgress: 'In Bearbeitung', inProgress: 'In Bearbeitung',
enterRootCause: 'Grundursache eingeben',
enterIncidentTitle: 'Vorfalltitel eingeben',
enterIncidentDescription: 'Vorfallbeschreibung eingeben',
enterServiceId: 'Service-IDs durch Komma getrennt eingeben',
selectAssignedUser: 'Zugewiesenen Benutzer auswählen',
noAssignedUser: 'Der Vorfall ist derzeit keinem Benutzer zugewiesen',
affectedSystems: 'Betroffene Systeme',
enterAffectedSystems: 'Betroffene Systeme eingeben',
separateSystemsWithComma: 'Betroffene Systeme durch Komma getrennt eingeben',
}; };
@@ -64,17 +64,4 @@ export const maintenanceTranslations: MaintenanceTranslations = {
noScheduledMaintenance: 'Keine geplanten Wartungen', noScheduledMaintenance: 'Keine geplanten Wartungen',
noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.', noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.',
maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt', maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt',
editMaintenanceWindow: 'Wartung bearbeiten',
editMaintenanceDesc: 'Wartungsdetails bearbeiten',
priorityDescription: 'Wartungspriorität auswählen',
statusDescription: 'Wartungsstatus auswählen',
impactLevel: 'Auswirkungsgrad',
impactLevelDescription: 'Auswirkungsgrad auswählen',
selectAssignedUsers: 'Zugewiesene Benutzer auswählen',
assignedPersonnelDescription: 'Derzeit zugewiesene Benutzer',
mutedNotifications: 'Benachrichtigungen deaktiviert',
notificationsAreMuted: 'Benachrichtigungen sind für diese Wartung derzeit deaktiviert',
selectNotificationChannel: 'Benachrichtigungskanal hinzufügen',
enableNotificationsFirst: 'Aktivieren Sie zuerst Benachrichtigungen, um einen Kanal auszuwählen',
updateMaintenance: 'Wartung aktualisieren',
}; };
+4 -89
View File
@@ -1,98 +1,13 @@
import { ServicesTranslations } from '../types/services'; import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = { export const servicesTranslations: ServicesTranslations = {
// Service Table // Dienst-Tabelle
serviceName: "Dienstname",
serviceType: "Diensttyp",
serviceStatus: "Dienststatus", serviceStatus: "Dienststatus",
responseTime: "Antwortzeit", responseTime: "Antwortzeit",
uptime: "Betriebszeit", uptime: "Betriebszeit",
lastChecked: "Zuletzt überprüft", lastChecked: "Zuletzt überprüft",
noServices: "Keine Dienste entsprechen Ihren Filterkriterien.", noServices: "Keine Dienste entsprechen Ihren Filterkriterien.",
currentlyMonitoring: "Derzeit überwacht",
retry: "Wiederholen",
overview: "Überblick",
newService: "Neuer Dienst",
rowsPerPage: "Zeilen pro Seite",
search: "Suchen",
allTypes: "Alle Arten",
createNewService: "Neuen Service hinzufügen",
createNewServiceDesc: "Geben Sie detaillierte Informationen ein, um einen neuen zu erstellen, den Sie überwachen möchten.",
// ServiceBasicFields.tsx
serviceName: "Dienstname",
serviceNameDesc: "Geben Sie einen aussagekräftigen Namen für Ihren Service ein",
// ServiceConfigFields.tsx
checkInterval: "Prüfintervall",
seconds: "Sekunden",
minute: "Minute",
minutes: "Minuten",
hour: "Stunde",
hours: "Stunden",
custom: "Benutzerdefiniert",
checkIntervalPlaceholder: "Intervall in Sekunden eingeben",
backToPresets: "Zurück zu Voreinstellungen",
checkIntervalDesc: "Wie oft der Dienststatus überprüft werden soll",
checkIntervalDescCustom: "Benutzerdefiniertes Intervall in Sekunden eingeben (mindestens 10 Sekunden)",
retryAttempts: "Wiederholungsversuche",
attempt: "Versuch",
attempts: "Versuche",
retryAttemptsDesc: "Anzahl der Wiederholungsversuche, bevor der Dienst als ausgefallen markiert wird",
// ServiceForm.tsx
updateService: "Dienst aktualisieren",
createService: "Dienst erstellen",
// ServiceNotificationFields.tsx
enableNotifications: "Benachrichtigungen aktivieren",
enableNotificationsDesc: "Benachrichtigungen für diesen Dienst aktivieren oder deaktivieren",
notificationChannels: "Benachrichtigungskanäle",
notificationChannelsEnabledDesc: "Benachrichtigungskanäle für diesen Dienst auswählen",
notificationChannelsDesc: "Aktivieren Sie zuerst Benachrichtigungen, um Kanäle auszuwählen",
notificationChannelsPlaceholder: "Benachrichtigungskanal hinzufügen",
alertTemplate: "Alarmvorlage",
alertTemplateLoading: "Lade Vorlagen...",
alertTemplatePlaceholder: "Alarmvorlage auswählen",
alertTemplateEnabledDesc: "Wählen Sie eine Vorlage für Alarmmeldungen",
alertTemplateDesc: "Aktivieren Sie zuerst Benachrichtigungen, um eine Vorlage auszuwählen",
// ServiceTypeField.tsx
serviceType: "Diensttyp",
serviceTypeHTTPDesc: "Webseiten und REST-APIs mit HTTP/HTTPS-Protokoll überwachen",
serviceTypePINGDesc: "Host-Verfügbarkeit mit PING-Protokoll überwachen",
serviceTypeTCPDesc: "TCP-Port-Konnektivität mit TCP-Protokoll überwachen",
serviceTypeDNSDesc: "DNS-Auflösung überwachen",
// ServiceRegionalFields.tsx
regionalMonitoring: "Regionale Überwachung",
regionalMonitoringDesc: "Diesen Dienst regionalen Überwachungsagenten für verteilte Überwachung zuweisen",
regionalAgents: "Regionale Agenten",
regionalAgentsLoading: "Lade Agenten...",
regionalAgentsAvailablePlaceholder: "Zusätzliche regionale Agenten auswählen...",
regionalAgentsAllSelected: "Alle verfügbaren Agenten ausgewählt",
regionalAgentsNoAvailable: "Keine regionalen Agenten verfügbar",
regionalAgentsNoOnlineAvailable: "Keine regionalen Agenten online verfügbar",
regionalAgentsNotFoundMessage: "Keine regionalen Online-Agenten gefunden. Dienste verwenden die Standardüberwachung.",
regionalAgentsNotSelectedMessage: "Keine regionalen Agenten ausgewählt. Der Dienst verwendet die Standardüberwachung.",
// ServiceUrlField.tsx
targetDefault: "Ziel-URL/Host",
targetDNS: "Domainname",
targetHTTPDesc: "Geben Sie die vollständige URL inklusive Protokoll ein (http:// oder https://)",
targetPINGDesc: "Geben Sie den Hostnamen oder die IP-Adresse zum Pingen ein",
targetTCPDesc: "Geben Sie Hostname oder IP-Adresse für TCP-Verbindungstest ein",
targetTCPPortDesc: "Geben Sie die Portnummer für den TCP-Verbindungstest ein",
targetDNSDesc: "Domainnamen für DNS-Überwachung eingeben (A, AAAA, MX, etc.)",
targetDefaultDesc: "Geben Sie die Ziel-URL oder den Hostnamen für die Überwachung ein",
targetDefaultPlaceholder: "URL oder Hostname eingeben",
// types.ts
serviceNameRequired: "Dienstname ist erforderlich",
urlDomainHostRequired: "URL/Domain/Host ist erforderlich",
enterValidUrlHostnameDomain: "Bitte eine gültige URL, einen Hostnamen oder eine Domain eingeben",
// Dashboard
upServices: "DIENSTE ONLINE",
downServices: "DIENSTE OFFLINE",
pausedServices: "DIENSTE PAUSIERT",
warningServices: "DIENSTE MIT WARNUNG",
}; };
+1 -8
View File
@@ -47,12 +47,5 @@ export const settingsTranslations: SettingsTranslations = {
testConnection: "Verbindung testen", testConnection: "Verbindung testen",
testingConnection: "Verbindung wird getestet...", testingConnection: "Verbindung wird getestet...",
connectionSuccess: "Verbindung erfolgreich", connectionSuccess: "Verbindung erfolgreich",
connectionFailed: "Verbindung fehlgeschlagen", connectionFailed: "Verbindung fehlgeschlagen"
// Ergänzte fehlende Einträge
addUser: "Benutzer hinzufügen",
permissionNotice: "Berechtigungshinweis:",
permissionNoticeAddUser: "Als Admin-Benutzer haben Sie keinen Zugriff auf die Anzeige oder Änderung von System- und E-Mail-Einstellungen. Diese Einstellungen können nur von Super-Admins aufgerufen und geändert werden. Wenden Sie sich an Ihren Super-Admin, wenn Sie Änderungen an der Systemkonfiguration oder den E-Mail-Einstellungen vornehmen müssen.",
loadingSettings: "Einstellungen werden geladen...",
loadingSettingsError: "Fehler beim Laden der Einstellungen",
}; };
+1 -2
View File
@@ -112,7 +112,6 @@ export const sslTranslations: SSLTranslations = {
created: "Erstellt", created: "Erstellt",
lastUpdated: "Zuletzt aktualisiert", lastUpdated: "Zuletzt aktualisiert",
lastNotification: "Letzte Benachrichtigung", lastNotification: "Letzte Benachrichtigung",
collectionId: "Sammlungs-ID", collectionId: "Sammlungs-ID"
noCertificatesFound: "Keine Zertifikate gefunden",
} }
+1 -17
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about'; import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = { export const aboutTranslations: AboutTranslations = {
aboutCheckCle: "About Checkcle", aboutCheckcle: "About Checkcle",
systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.", systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.",
systemVersion: "System Version", systemVersion: "System Version",
license: "License", license: "License",
@@ -15,20 +15,4 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "Quick Actions", quickActions: "Quick Actions",
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.", quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
quickTips: "Quick Tips", quickTips: "Quick Tips",
releasedOn: "Released On",
updateSchema: "Update Schema",
updateSchemaDesc: "Automatic import collections schema",
mergeFieldsLabel: "Merge fields with existing collections (safe - preserves data)",
importing: "Importing...",
clickToUpdateSchema: "Click to update Schema",
importSuccessful: "Import Successful",
importFailed: "Import Failed",
instructions: "Instructions",
mergeFields: "Merge fields",
instructionsMergeFields: "Safely add new fields to existing collections, preserves all data",
instructionsCollections: "System collections (starting with _) and users collection will be skipped automatically",
instructionsImportAuth: "Only authenticated admins can perform schema imports",
collectionsUpdatedCount: "{count} collections updated",
collectionsCreatedCount: "{count} collections created",
collectionsSkippedCount: "{count} collections created",
}; };
+3 -5
View File
@@ -5,12 +5,10 @@ export const commonTranslations: CommonTranslations = {
welcome: "Welcome", welcome: "Welcome",
logout: "Logout", logout: "Logout",
language: "Language", language: "Language",
english: "Englisch", english: "English",
khmer: "ភាសាខ្មែរ", khmer: "Khmer",
german: "Deutsch", german: "Deutsch",
korean: "한국어", simplifiedChinese: "Simplified Chinese",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "Good morning", goodMorning: "Good morning",
goodAfternoon: "Good afternoon", goodAfternoon: "Good afternoon",
goodEvening: "Good evening", goodEvening: "Good evening",
@@ -52,14 +52,4 @@ export const incidentTranslations: IncidentTranslations = {
configuration: 'Configuration', configuration: 'Configuration',
failedToUpdateStatus: 'Failed to update status', failedToUpdateStatus: 'Failed to update status',
inProgress: 'In Progress', inProgress: 'In Progress',
enterRootCause: 'Enter Root Cause',
enterIncidentTitle: 'Enter Incident Title',
enterIncidentDescription: 'Enter Incident Description',
enterServiceId: 'Enter Service-IDs seperated by comma',
selectAssignedUser: 'Select Assigned User',
noAssignedUser: 'Incident is currently not assigned to any user',
affectedSystems: 'Affected Systems',
enterAffectedSystems: 'Enter Affected Systems',
separateSystemsWithComma: 'Enter affected systems seperated by comma',
createIncidentDesc: 'Create a new incident',
}; };
@@ -65,25 +65,4 @@ export const maintenanceTranslations: MaintenanceTranslations = {
noScheduledMaintenance: 'No Scheduled Maintenance', noScheduledMaintenance: 'No Scheduled Maintenance',
noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.', noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.',
maintenanceCreatedSuccess: 'Maintenance window created successfully', maintenanceCreatedSuccess: 'Maintenance window created successfully',
editMaintenanceWindow: 'Edit Maintenance',
editMaintenanceDesc: 'Edit maintenance details',
priorityDescription: 'Select maintenance priority',
statusDescription: 'Select maintenance status',
impactLevel: 'Impact Level',
impactLevelDescription: 'Select impact level',
selectAssignedUsers: 'Select Assigned Users',
assignedPersonnelDescription: 'Currently assigned users',
mutedNotifications: 'Notifications disabled',
notificationsAreMuted: 'Notifications are currently muted for this maintenance',
selectNotificationChannel: 'Add a notification channel',
enableNotificationsFirst: 'Enable notifications first to select channel',
updateMaintenance: 'Update Maintenance',
loginToViewProfile: 'Please login to view your profile',
goToLogin: 'Go to Login',
loadingUserData: 'Loading user data...',
retry: 'Retry',
loadingServerData: 'Loading server data...',
retrievingYourInformation: 'Please wait while we retrieve your information...',
servicesPagination: '{startItem}-{endItem} of {totalItems} services',
servicesPaginationNoService: '0 services',
}; };
+3 -130
View File
@@ -2,138 +2,11 @@
import { ServicesTranslations } from '../types/services'; import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = { export const servicesTranslations: ServicesTranslations = {
serviceName: "Service Name",
serviceType: "Service Type",
serviceStatus: "Service Status", serviceStatus: "Service Status",
responseTime: "Response Time",
uptime: "Uptime", uptime: "Uptime",
lastChecked: "Last Checked", lastChecked: "Last Checked",
noServices: "No services match your filter criteria.", noServices: "No services match your filter criteria.",
currentlyMonitoring: "Currently Monitoring",
retry: "Retry",
overview: "Overview",
newService: "NewService",
rowsPerPage: "Rows Per Page",
search: "Search",
allTypes: "All Types",
createNewService: "Create New Service",
createNewServiceDesc: "Fill in the details to create a new service to monitor.",
// ServiceBasicFields.tsx
serviceName: "Service Name",
serviceNameDesc: "Enter a descriptive name for your service",
// ServiceConfigFields.tsx
checkInterval: "Check Interval",
seconds: "seconds",
minute: "minute",
minutes: "minutes",
hour: "hour",
hours: "hours",
custom: "Custom",
checkIntervalPlaceholder: "Enter interval in seconds",
backToPresets: "Back to presets",
checkIntervalDesc: "How often to check the service status",
checkIntervalDescCustom: "Enter custom interval in seconds (minimum 10 seconds)",
retryAttempts: "Retry Attempts",
attempt: "attempt",
attempts: "attempts",
retryAttemptsDesc: "Number of retry attempts before marking as down",
// ServiceForm.tsx
updateService: "Update Service",
createService: "Create Service",
// ServiceNotificationFields.tsx
enableNotifications: "Enable Notifications",
enableNotificationsDesc: "Enable or disable notifications for this service",
notificationChannels: "Notification Channels",
notificationChannelsEnabledDesc: "Select notification channels for this service",
notificationChannelsDesc: "Enable notifications first to select channels",
notificationChannelsPlaceholder: "Add a notification channel",
alertTemplate: "Alert Template",
alertTemplateLoading: "Loading templates...",
alertTemplatePlaceholder: "Select an alert template",
alertTemplateEnabledDesc: "Choose a template for alert messages",
alertTemplateDesc: "Enable notifications first to select template",
// ServiceTypeField.tsx
serviceType: "Service Type",
serviceTypeHTTPDesc: "Monitor websites and REST APIs with HTTP/HTTPS Protocol",
serviceTypePINGDesc: "Monitor host availability with PING Protocol",
serviceTypeTCPDesc: "Monitor TCP port connectivity with TCP Protocol",
serviceTypeDNSDesc: "Monitor DNS resolution",
// ServiceRegionalFields.tsx
regionalMonitoring: "Regional Monitoring",
regionalMonitoringDesc: "Assign this service to regional monitoring agents for distributed monitoring",
regionalAgents: "Regional Agents",
regionalAgentsLoading: "Loading agents...",
regionalAgentsAvailablePlaceholder: "Select additional regional agents...",
regionalAgentsAllSelected: "All available agents selected",
regionalAgentsNoAvailable: "No regional agents available",
regionalAgentsNoOnlineAvailable: "No online regional agents available",
regionalAgentsNotFoundMessage: "No online regional agents found. Services will use default monitoring.",
regionalAgentsNotSelectedMessage: "No regional agents selected. Service will use default monitoring.",
// ServiceUrlField.tsx
targetDefault: "Target URL/Host",
targetDNS: "Domain Name",
targetHTTPDesc: "Enter the full URL including protocol (http:// or https://)",
targetPINGDesc: "Enter hostname or IP address to ping",
targetTCPDesc: "Enter hostname or IP address for TCP connection test",
targetTCPPortDesc: "Enter the port number for TCP connection test",
targetDNSDesc: "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)",
targetDefaultDesc: "Enter the target URL or hostname for monitoring",
targetDefaultPlaceholder: "Enter URL or hostname",
// types.ts
serviceNameRequired: "Service name is required",
urlDomainHostRequired: "URL/Domain/Host is required",
enterValidUrlHostnameDomain: "Please enter a valid URL, hostname, or domain",
// Dashboard
upServices: "UP SERVICES",
downServices: "DOWN SERVICES",
pausedServices: "PAUSED SERVICES",
warningServices: "WARNING SERVICES",
// ServiceRowActions.tsx
viewDetail: "View Detail",
resumeMonitoring: "Resume Monitoring",
pauseMonitoring: "Pause Monitoring",
//IncidentTable.tsx
responseTime: "Response Time",
errorMessage: "Error Message",
details: "Details",
unmuteAlerts: "Unmute Alerts",
muteAlerts: "Mute Alerts",
//LastCheckedTime.tsx
pausedAt: "Paused at ",
lastCheckDetails: "Last Check Details",
checkedAt: "Checked at ",
monitoringPaused: "Monitoring Paused",
noAutomaticChecks: "No automatic checks",
//ServiveEditDialog.tsx
editService: "Edit Service",
editServiceDesc: "Update the details of your monitored service.",
//ServiceStatsCards.tsx
lastCheckedAt: "Last checked at {datetime}",
avg: "Avg",
lastUpChecksCount: "last {count} up checks",
basedOnlastChecksCount: "Based on last {count} checks",
totalUptime: "Total Uptime",
totalDowntime: "Total Downtime",
monitoringSettings: "Monitoring Settings",
monitoringSettingsInterval: "Checked every {interval} seconds",
monitoringSettingsType: "monitoring",
upStatusDuration: "Up for {duration}",
downStatusDuration: "Down for {duration}",
incidentHistory: "Incident History",
processing: "Processing",
back: "Back",
last20Checks: "Last 20 checks",
}; };
+6 -17
View File
@@ -2,18 +2,18 @@
import { SettingsTranslations } from '../types/settings'; import { SettingsTranslations } from '../types/settings';
export const settingsTranslations: SettingsTranslations = { export const settingsTranslations: SettingsTranslations = {
// General Settings - Tabs // Tabs
systemSettings: "System Settings", systemSettings: "System Settings",
mailSettings: "Mail Settings", mailSettings: "Mail Settings",
// General Settings - System Settings // System Settings
appName: "Application Name", appName: "Application Name",
appURL: "Application URL", appURL: "Application URL",
senderName: "Sender Name", senderName: "Sender Name",
senderEmail: "Sender Email Address", senderEmail: "Sender Email Address",
hideControls: "Hide Controls", hideControls: "Hide Controls",
// General Settings - Mail Settings // Mail Settings
smtpSettings: "SMTP Configuration", smtpSettings: "SMTP Configuration",
smtpEnabled: "Enable SMTP", smtpEnabled: "Enable SMTP",
smtpHost: "SMTP Host", smtpHost: "SMTP Host",
@@ -24,7 +24,7 @@ export const settingsTranslations: SettingsTranslations = {
enableTLS: "Enable TLS", enableTLS: "Enable TLS",
localName: "Local Name", localName: "Local Name",
// General Settings - Test Email // Test Email
testEmail: "Test Email", testEmail: "Test Email",
sendTestEmail: "Send test email", sendTestEmail: "Send test email",
emailTemplate: "Email template", emailTemplate: "Email template",
@@ -37,13 +37,9 @@ export const settingsTranslations: SettingsTranslations = {
selectCollection: "Select collection", selectCollection: "Select collection",
toEmailAddress: "To email address", toEmailAddress: "To email address",
enterEmailAddress: "Enter email address", enterEmailAddress: "Enter email address",
send: "Send",
sending: "Sending...", sending: "Sending...",
testEmailSettings: "Test Email Settings",
testEmailDescription: "Test whether the current Email Settings are available",
testEmailAlert: "This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.",
// General Settings - Actions and status // Actions and status
save: "Save Changes", save: "Save Changes",
saving: "Saving...", saving: "Saving...",
settingsUpdated: "Settings updated successfully", settingsUpdated: "Settings updated successfully",
@@ -52,12 +48,5 @@ export const settingsTranslations: SettingsTranslations = {
testConnection: "Test Connection", testConnection: "Test Connection",
testingConnection: "Testing Connection...", testingConnection: "Testing Connection...",
connectionSuccess: "Connection successful", connectionSuccess: "Connection successful",
connectionFailed: "Connection failed", connectionFailed: "Connection failed"
// User Management
addUser: "Add User",
permissionNotice: "Permission Notice:",
permissionNoticeAddUser: "As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.",
loadingSettings: "Loading settings...",
loadingSettingsError: "Error loading settings",
}; };
+1 -2
View File
@@ -112,6 +112,5 @@ export const sslTranslations: SSLTranslations = {
created: "Created", created: "Created",
lastUpdated: "Last Updated", lastUpdated: "Last Updated",
lastNotification: "Last Notification", lastNotification: "Last Notification",
collectionId: "Collection ID", collectionId: "Collection ID"
noCertificatesFound: "No Certificates Found",
}; };
+1 -3
View File
@@ -1,17 +1,15 @@
import enTranslations from './en'; import enTranslations from './en';
import kmTranslations from './km'; import kmTranslations from './km';
import deTranslations from './de'; import deTranslations from './de';
import koTranslations from './ko';
import jaTranslations from './ja'; import jaTranslations from './ja';
import zhcnTranslations from './zhcn'; import zhcnTranslations from './zhcn';
export type Language = "en" | "km" | "de" | "ko" | "ja" | "zhcn"; export type Language = "en" | "km" | "de" | "ja" | "zhcn";
export const translations = { export const translations = {
en: enTranslations, en: enTranslations,
km: kmTranslations, km: kmTranslations,
de: deTranslations, de: deTranslations,
ko: koTranslations,
ja: jaTranslations, ja: jaTranslations,
zhcn: zhcnTranslations, zhcn: zhcnTranslations,
}; };
+1 -2
View File
@@ -1,7 +1,7 @@
import { AboutTranslations } from '../types/about'; import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = { export const aboutTranslations: AboutTranslations = {
aboutCheckCle: "Checkcleについて", aboutCheckcle: "Checkcleについて",
systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。", systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。",
systemVersion: "システムバージョン", systemVersion: "システムバージョン",
license: "ライセンス", license: "ライセンス",
@@ -14,5 +14,4 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "クイックアクション", quickActions: "クイックアクション",
quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。", quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。",
quickTips: "クイックヒント", quickTips: "クイックヒント",
releasedOn: "公開日",
}; };
+3 -4
View File
@@ -4,12 +4,11 @@ export const commonTranslations: CommonTranslations = {
welcome: "ようこそ", welcome: "ようこそ",
logout: "ログアウト", logout: "ログアウト",
language: "言語", language: "言語",
english: "Englisch", english: "English",
khmer: "ភាសាខ្មែរ", khmer: "Khmer",
german: "Deutsch", german: "Deutsch",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文", simplifiedChinese: "简体中文",
japanese: "日本語",
goodMorning: "おはようございます", goodMorning: "おはようございます",
goodAfternoon: "こんにちは", goodAfternoon: "こんにちは",
goodEvening: "こんばんは", goodEvening: "こんばんは",
@@ -51,5 +51,4 @@ export const incidentTranslations: IncidentTranslations = {
configuration: '設定', configuration: '設定',
failedToUpdateStatus: 'ステータスの更新に失敗しました', failedToUpdateStatus: 'ステータスの更新に失敗しました',
inProgress: '進行中', inProgress: '進行中',
enterRootCause: '原因を入力してください',
}; };
@@ -2,20 +2,10 @@ import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = { export const servicesTranslations: ServicesTranslations = {
serviceName: "サービス名", serviceName: "サービス名",
serviceNameDesc: "サービスのわかりやすい名前を入力してください",
serviceType: "サービスタイプ", serviceType: "サービスタイプ",
serviceStatus: "サービスステータス", serviceStatus: "サービスステータス",
responseTime: "応答時間", responseTime: "応答時間",
uptime: "稼働時間", uptime: "稼働時間",
lastChecked: "最終チェック", lastChecked: "最終チェック",
noServices: "フィルタ条件に一致するサービスがありません。", noServices: "フィルタ条件に一致するサービスがありません。",
currentlyMonitoring: "現在監視中",
retry: "再試行",
overview: "概要",
newService: "新しいサービス",
rowsPerPage: "1ページあたりの行数",
search: "検索",
allTypes: "すべてのタイプ",
createNewService: "新しいサービスを作成",
createNewServiceDesc: "監視する新しいサービスを作成するには、詳細を入力してください。",
}; };
+1 -2
View File
@@ -111,6 +111,5 @@ export const sslTranslations: SSLTranslations = {
created: "作成日", created: "作成日",
lastUpdated: "最終更新", lastUpdated: "最終更新",
lastNotification: "最終通知", lastNotification: "最終通知",
collectionId: "コレクションID", collectionId: "コレクションID"
noCertificatesFound: "証明書が見つかりません",
}; };
+1 -2
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about'; import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = { export const aboutTranslations: AboutTranslations = {
aboutCheckCle: "អំពី Checkcle", aboutCheckcle: "អំពី Checkcle",
systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។", systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។",
systemVersion: "កំណែប្រព័ន្ធ", systemVersion: "កំណែប្រព័ន្ធ",
license: "អាជ្ញាបណ្ណ", license: "អាជ្ញាបណ្ណ",
@@ -15,5 +15,4 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "សកម្មភាពរហ័ស", quickActions: "សកម្មភាពរហ័ស",
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។", quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
quickTips: "គន្លឹះរហ័ស", quickTips: "គន្លឹះរហ័ស",
releasedOn: "បានចេញផ្សាយនៅថ្ងៃទី",
}; };

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