36 Commits

Author SHA1 Message Date
Phearun UM bda862e4d3 menu collaged full view and font size adjustment (#162)
* menu sticky and add font adjustment

* menu sticky and add font adjustment

* ui menu sticky and fontsize adjustment

---------

Co-authored-by: phearun <phearun@whitesand.online>
2025-09-15 13:43:22 +07:00
Tola Leng 6887dc291b Merge pull request #172 from EchtkPvL/develop
Fix YAML syntax error
2025-09-14 21:01:03 +07:00
Jonas Berner 29daddfc3b Fix YAML syntax error 2025-09-14 12:32:54 +02:00
Tola Leng 9c44ba6d7f Fix: Improve Uptime History Bar Display Speed Immediately
- Optimize the UptimeBar component to display uptime data immediately and reduce loading delays and background updates
2025-09-14 02:52:55 +07:00
Tola Leng c61a740725 feat: Add schema database update for release v1.6.0
- Applied new database schema updates.
- Ensured compatibility with data retention service and notification features.
- Keeps database in sync with new features introduced in v1.6.0.
2025-09-13 22:24:37 +07:00
Tola Leng a9d3d86394 chore(project): Update development status and progress 2025-09-13 22:22:08 +07:00
Tola Leng 9b0b2663ec refactor: Remove manual cleanup buttons from Data Retention Settings
- The three manual cleanup buttons have been removed from the dashboard.
- The data retention service now automatically runs once per day, cleaning up older data based on the configured retention settings.
2025-09-13 02:28:52 +07:00
Tola Leng 0b04034141 Merge pull request #159 from sqkkyzx/develop
feat(i18n): Refactored i18n function to support multiple parameter combinations
2025-09-12 15:57:24 +07:00
Tola Leng 0a635ab3aa feat: Integrate data retention service
- Implemented the data retention service in Go (service-operation) that manages cleanup of old records based on configured retention periods. The service runs once per day to ensure outdated data is removed automatically.
2025-09-11 22:43:07 +07:00
YiZixuan 175d2b6329 docs(README_zhcn): Update documentation content and sponsorship policy
- Update image links in the documentation
- Revise the sponsorship policy to no longer accept individual sponsorships, only enterprise-level collaborations
- Update the display method for supporters and partners
- Add Cloudflare and DigitalOcean as new partners
2025-09-10 00:19:14 +08:00
YiZixuan 3d6c4151b8 Merge branch 'operacle:develop' into develop 2025-09-09 22:40:37 +08:00
YiZixuan 5f893e3b20 refactor(i18n): Optimize service pagination internationalization copy
- Added service pagination related copy in English and Chinese translation files
- Updated type definitions to include new translation fields
- Modified the ServicesPagination component to use the newly added translation fields
2025-09-09 22:39:17 +08:00
YiZixuan 1296cf6a32 feat(i18n): Enhanced internationalization support with variable interpolation
- Refactored the `t` function in LanguageContext to support multiple parameter combinations
- Added variable interpolation feature, allowing variables in translated texts
- Optimized translation logic to improve accuracy and performance
- Enhanced type safety by using TypeScript generics to ensure correct typing
2025-09-09 22:38:43 +08:00
Tola Leng 31e5335aac docs(readme): deactivate donations, accept only infra support (cloud, domain, hosting) 2025-09-09 20:50:33 +07:00
YiZixuan b6d4ca5269 refactor(i18n): Optimize internationalization configuration and add new translation items
- Add internationalization support to the LoadingState component
- Implement internationalization translations in the Profile page
- Use internationalized text in the TestEmailDialog component
- Update English and Chinese translation files by adding new translation entries
2025-09-09 21:48:35 +08:00
Tola Leng 7e7522608a chore: deactivate GitHub donation 2025-09-09 20:39:09 +07:00
YiZixuan 43957f65d2 refactor(i18n): Added Chinese translations for Create Event and Maintenance
- Supplemented missing Chinese translations
- Added translation entries related to Create Event and Maintenance in both English and Chinese translation files
2025-09-09 21:21:34 +08:00
Tola Leng 09ece0a77e feat(auth): add token support for ntfy
- Updated the NTFY service to support API token authentication by adding Authorization Bearer header when api_token is provided.

Closes: #89
2025-09-09 20:21:20 +07:00
Tola Leng 06ba38aa15 feat: Add NTFY API token field for Token-based authentication to ntfy server
- Add the api_token field for NTFY notifications in both the dialog and service to save it to PB. NTFY schema to include api_token field.
2025-09-09 17:48:25 +07:00
Tola Leng abd878fe6c Merge pull request #158 from sqkkyzx/develop
refactory(i18n): Improve internationalization configuration and add new translations
2025-09-09 02:31:34 +07:00
YiZixuan f4d47bbd45 refactory(i18n): Add internationalization support for the service statistics card 2025-09-08 21:35:48 +08:00
YiZixuan b675fddc92 refactory(i18n): Added architecture import function translation for the "About" page
- Added translation items related to schema import in English, Chinese, and type definition files
- Replaced hard-coded text with new translation items in the AboutSystem component
2025-09-08 20:54:56 +08:00
YiZixuan 0a70ed2143 refactory(i18n): Added architecture import function translation for the "About" page
- Added translation items related to schema import in English, Chinese, and type definition files
- Replaced hard-coded text with new translation items in the AboutSystem component
2025-09-08 19:35:41 +08:00
YiZixuan 4e07279a54 refactory(i18n): Improve internationalization configuration and add new translations
- Added multiple new translation items
- Updated the two language files: en and zhcn
- Added type definitions for new translation items in types/services.ts
- Modified some components to use the new translation items
2025-09-08 14:18:05 +08:00
Tola Leng 020cdec912 chore(project): update development status and progress
- feat: Add server_url field for Gotify
- feat: Implement Gotify notification service
2025-09-06 20:44:28 +07:00
Tola Leng 4771b2ead0 feat: Implement Gotify notification service
- The Gotify notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `server_url` field for send to the Gotify API endpoint.
2025-09-06 20:42:22 +07:00
Tola Leng 87c829e100 feat: Add Gotify to notification channels
- Add Gotify  as a new channel type in the notification settings. The Gotify channel will use the `api_token` and `server_url` field for API key storage in PB.
2025-09-06 19:33:14 +07:00
Tola Leng e6093f9fe2 Fix: Remove duplicate close button
- Removed the duplicate close button from the test email dialog.
2025-09-04 22:05:18 +07:00
Tola Leng 956786a859 feat: add SSL history and Notifiarr notification channel schema 2025-09-03 22:31:20 +07:00
Tola Leng 6a0169e4d8 chore(project): update development status and progress
- feat: Add channel_id field for Notifiarr
- feat: Implement Notifiarr notification service

Closes: #133
2025-09-03 22:05:11 +07:00
Tola Leng 92959945f3 feat: Implement Notifiarr notification service
- The Notifiarr notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `channel_id` field for send to the Notifiarr API endpoint.
2025-09-03 21:58:40 +07:00
Tola Leng b2269187d8 feat: Add channel_id field for Notifiarr
- Add channel_id field for Notifiarr to store record of the discord channel_id into the PB.
2025-09-03 21:48:59 +07:00
Tola Leng 05f46969c4 feat: Add Notifiarr to notification channels (UI)
- Add Notifiarr as a new channel type in the notification settings. The Notifiarr channel will use the `api_token` field for API key storage in PB.
2025-09-02 21:37:54 +07:00
Tola Leng ab168b440e chore(project): update development status and progress
- fix(ssl): ensure edit form saves notification_id and template_id in DB
- feat(notifications): add Notifiarr notification channel (planned)
2025-08-29 19:58:24 +07:00
Tola Leng 2e866d7a95 chore(partners): add Cloudflare to Ecosystem & Community Partner section
- Cloudflare has been added to the Ecosystem & Community Partner list to acknowledge their support and contribution.
2025-08-29 19:53:39 +07:00
Tola Leng b372131978 fix(ssl): Ensure edit form saves notification_id and template_id in DB.
- The SSL edit form was not saving the `notification_id` and `template_id` fields to the PB database when re-assigning the Notification Channel and Alert Template.
2025-08-29 19:50:36 +07:00
27 changed files with 1422 additions and 579 deletions
+2 -1
View File
@@ -85,7 +85,8 @@ body:
id: terms id: terms
attributes: attributes:
label: Checklist label: Checklist
description: Please confirm the following before submitting: description: |
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
+2 -1
View File
@@ -51,7 +51,8 @@ body:
id: checklist id: checklist
attributes: attributes:
label: Checklist label: Checklist
description: Before submitting, please confirm: description: |
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
+2 -1
View File
@@ -45,7 +45,8 @@ body:
id: checklist id: checklist
attributes: attributes:
label: Checklist label: Checklist
description: Before submitting, please confirm the following: description: |
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
+24 -20
View File
@@ -1,25 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle</title>
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta property="og:title" content="checkcle-an-open-source" /> <head>
<meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." /> <meta charset="UTF-8" />
<meta property="og:type" content="website" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" /> <title>CheckCle - Dev</title>
<meta name="description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta name="twitter:card" content="summary_large_image" /> <meta property="og:title" content="checkcle-an-open-source" />
<meta name="twitter:site" content="@checkcle_oss" /> <meta property="og:description"
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" /> content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
</head> <meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<body> <meta name="twitter:card" content="summary_large_image" />
<div id="root"></div> <meta name="twitter:site" content="@checkcle_oss" />
<script type="module" src="/src/main.tsx"></script> <meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</body>
</html> </head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+184
View File
@@ -8,6 +8,7 @@
"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",
@@ -644,6 +645,21 @@
"@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",
@@ -663,6 +679,26 @@
"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",
@@ -2280,6 +2316,103 @@
"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",
@@ -2729,6 +2862,15 @@
"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",
@@ -2793,6 +2935,33 @@
"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",
@@ -6289,6 +6458,12 @@
"node": ">=12.0.0" "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",
@@ -6568,6 +6743,15 @@
} }
} }
}, },
"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",
+2
View File
@@ -11,6 +11,7 @@
"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",
@@ -45,6 +46,7 @@
"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",
@@ -154,7 +154,8 @@
"email", "email",
"ntfy", "ntfy",
"pushover", "pushover",
"notifiarr" "notifiarr",
"gotify"
] ]
}, },
{ {
@@ -339,6 +340,20 @@
"system": false, "system": false,
"type": "text" "type": "text"
}, },
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3401535476",
"max": 0,
"min": 0,
"name": "server_url",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{ {
"hidden": false, "hidden": false,
"id": "autodate2990389176", "id": "autodate2990389176",
@@ -5643,4 +5658,4 @@
"indexes": [], "indexes": [],
"system": false "system": false
} }
] ]
+139 -45
View File
@@ -1,37 +1,40 @@
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(() => new QueryClient({ const [queryClient] = useState(
defaultOptions: { () =>
queries: { new QueryClient({
staleTime: 60 * 1000 defaultOptions: {
} queries: {
} staleTime: 60 * 1000,
})); },
},
})
);
return ( return (
<BrowserRouter> <BrowserRouter>
@@ -43,22 +46,113 @@ function App() {
<Routes> <Routes>
{/* Public routes */} {/* Public routes */}
<Route path="/public/:slug" element={<PublicStatusPage />} /> <Route path="/public/:slug" element={<PublicStatusPage />} />
{/* Protected routes */} {/* Protected routes */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} /> <Route
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} /> path="/"
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} /> element={
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} /> <ProtectedRoute>
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} /> <Index />
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} /> </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
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} /> path="/dashboard"
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} /> element={
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} /> <ProtectedRoute>
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} /> <Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/instance-monitoring"
element={
<ProtectedRoute>
<InstanceMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/server-detail/:serverId"
element={
<ProtectedRoute>
<ServerDetail />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring/:serverId"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route
path="/regional-monitoring"
element={
<ProtectedRoute>
<RegionalMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>
@@ -69,4 +163,4 @@ function App() {
); );
} }
export default App; export default App;
@@ -1,4 +1,3 @@
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";
@@ -15,17 +14,24 @@ interface DashboardContentProps {
error: Error | null; error: Error | null;
} }
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => { export const DashboardContent = ({
services,
isLoading,
error,
}: DashboardContentProps) => {
const { t } = useLanguage(); 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 = service.name.toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch =
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase())); service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase(); (service.url &&
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter =
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
return matchesSearch && matchesFilter; return matchesSearch && matchesFilter;
}); });
@@ -33,40 +39,42 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
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()}>{t("retry")}</Button>
</div> </div>
); );
} }
return ( return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0"> <main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl ">
<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">{t('overview')}</h2> <h2 className="text-2xl font-bold text-foreground">
<Button {t("overview")}
</h2>
<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" /> {t("newService")}
</Button> </Button>
</div> </div>
<StatusCards services={services} /> <StatusCards services={services} />
<ServiceFilters <ServiceFilters
filter={filter} filter={filter}
setFilter={setFilter} setFilter={setFilter}
searchTerm={searchTerm} searchTerm={searchTerm}
setSearchTerm={setSearchTerm} setSearchTerm={setSearchTerm}
servicesCount={filteredServices.length} servicesCount={filteredServices.length}
/> />
<div className="flex-1 flex flex-col pb-6"> <div className="flex-1 flex flex-col pb-6">
<ServicesTable services={filteredServices} /> <ServicesTable services={filteredServices} />
</div> </div>
</div> </div>
<AddServiceDialog <AddServiceDialog
open={isAddDialogOpen} open={isAddDialogOpen}
onOpenChange={setIsAddDialogOpen} onOpenChange={setIsAddDialogOpen}
/> />
+208 -57
View File
@@ -1,13 +1,39 @@
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 { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react"; import {
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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react"; import { 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;
@@ -20,7 +46,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();
@@ -55,148 +81,270 @@ 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" className="w-full h-full dark:bg-black"
style={{ style={{
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px), backgroundImage: `linear-gradient(${
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`, theme === "dark" ? "#ffffff10" : "#00000010"
backgroundSize: '20px 20px' } 1px, transparent 1px),
linear-gradient(90deg, ${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
}} }}
> >
<div className="w-full h-full backdrop-blur-[1px]"></div> <div className="w-full h-full backdrop-blur-[1px]"></div>
</div> </div>
</div> </div>
{/* Header Content */} {/* Header Content */}
<div className="flex items-center gap-4 z-10"> <div className="flex items-center gap-4 z-10">
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2"> <div
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />} className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${
!sidebarCollapsed ? "hidden" : ""
}`}
>
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className={`h-6 w-6 `}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="mr-2"
>
{sidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</Button> </Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1> <h1 className="font-medium text-xl">
{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 variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}> <Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={toggleTheme}
>
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />} {theme === "dark" ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border"> <Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span> <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 onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("en")}
className={language === "en" ? "bg-accent" : ""}
>
{t("english")} {t("english")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("km")}
className={language === "km" ? "bg-accent" : ""}
>
{t("khmer")} {t("khmer")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("de")}
className={language === "de" ? "bg-accent" : ""}
>
{t("Deutsch")} {t("Deutsch")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("ko")}
className={language === "ko" ? "bg-accent" : ""}
>
{t("korean")} {t("korean")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("ja")}
className={language === "ja" ? "bg-accent" : ""}
>
{t("japanese")} {t("japanese")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}> <DropdownMenuItem
onClick={() => setLanguage("zhcn")}
className={language === "zhcn" ? "bg-accent" : ""}
>
{t("simplifiedChinese")} {t("simplifiedChinese")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Documentation */} {/* Documentation */}
<Button <Button
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={() => window.open("https://docs.checkcle.io", "_blank")} onClick={() => window.open("https://docs.checkcle.io", "_blank")}
> >
<span className="sr-only">{t("documentation")}</span> <span className="sr-only">{t("documentation")}</span>
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
</Button> </Button>
{/* GitHub */} {/* GitHub */}
<Button <Button
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={() => window.open("https://github.com/operacle/checkcle", "_blank")} onClick={() =>
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" />
</Button> </Button>
{/* X (Twitter) */} {/* X (Twitter) */}
<Button <Button
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={() => window.open("https://x.com/checkcle_oss)", "_blank")} onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
> >
<span className="sr-only">X (Twitter)</span> <span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" /> <Twitter className="w-4 h-4" />
</Button> </Button>
{/* Discord */} {/* Discord */}
<Button <Button
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={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")} onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
> >
<span className="sr-only">Discord</span> <span className="sr-only">Discord</span>
<MessageSquare className="w-4 h-4" /> <MessageSquare className="w-4 h-4" />
</Button> </Button>
{/* Notifications */} {/* Notifications */}
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border"> <Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("notifications")}</span> <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 ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary"> {avatarUrl ? (
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'} <AvatarImage
</AvatarFallback>} src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar> </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 ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary"> {avatarUrl ? (
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'} <AvatarImage
</AvatarFallback>} src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar> </Avatar>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<span className="font-medium">{currentUser?.name || 'User'}</span> <span className="font-medium">
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span> {currentUser?.name || "User"}
</span>
<span className="text-xs text-muted-foreground truncate">
{currentUser?.email}
</span>
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -209,7 +357,10 @@ export const Header = ({
<span>{t("settings")}</span> <span>{t("settings")}</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500"> <DropdownMenuItem
onClick={onLogout}
className="text-red-500 focus:text-red-500"
>
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>{t("logout")}</span> <span>{t("logout")}</span>
</DropdownMenuItem> </DropdownMenuItem>
@@ -1,7 +1,12 @@
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 { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
@@ -13,29 +18,31 @@ interface ServiceFiltersProps {
servicesCount: number; servicesCount: number;
} }
export const ServiceFilters = ({ export const ServiceFilters = ({
filter, filter,
setFilter, setFilter,
searchTerm, searchTerm,
setSearchTerm, setSearchTerm,
servicesCount servicesCount,
}: ServiceFiltersProps) => { }: ServiceFiltersProps) => {
const { t } = useLanguage(); 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">{t('currentlyMonitoring')}</h3> <h3 className="text-xl font-semibold mr-2 text-foreground">
{t("currentlyMonitoring")}
</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm"> <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"> <SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
<SelectValue placeholder={t('allTypes')} /> <SelectValue placeholder={t("allTypes")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">{t('allTypes')}</SelectItem> <SelectItem value="all">{t("allTypes")}</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>
@@ -43,9 +50,9 @@ export const ServiceFilters = ({
</SelectContent> </SelectContent>
</Select> </Select>
<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={t("search")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
@@ -1,4 +1,3 @@
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";
@@ -11,18 +10,25 @@ interface SidebarProps {
export const Sidebar = ({ collapsed }: SidebarProps) => { export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<div <div
className={` className={`
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} ${
border-r flex flex-col h-full theme === "dark"
${collapsed ? 'w-16' : 'w-64'} ? "bg-black border-[#1e1e1e]"
`} : "bg-sidebars border-sidebar-border"
}
border-r flex flex-col h-full
${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>
); );
}; };
@@ -1,4 +1,3 @@
import React from "react"; import React from "react";
import { MenuItem } from "./MenuItem"; import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData"; import { mainMenuItems } from "./navigationData";
@@ -7,9 +6,15 @@ interface MainNavigationProps {
collapsed: boolean; collapsed: boolean;
} }
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => { export const MainNavigation: React.FC<MainNavigationProps> = ({
collapsed,
}) => {
return ( return (
<nav className="my-2 mx-1 py-1 px-1"> <nav
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}
@@ -24,4 +29,4 @@ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) =>
))} ))}
</nav> </nav>
); );
}; };
@@ -1,4 +1,3 @@
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";
@@ -22,7 +21,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();
@@ -32,7 +31,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (hasNavigation && path) { if (hasNavigation && path) {
navigate(path, { replace: false }); navigate(path, { replace: false });
} }
@@ -42,23 +41,29 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const mainIconSize = "h-6 w-6"; const mainIconSize = "h-6 w-6";
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 text-[15px]"> <span className="ml-2.5 font-medium text-foreground tracking-wide ">
{t(translationKey)} {t(translationKey)}
</span> </span>
)} )}
</div> </div>
); );
}; };
@@ -1,10 +1,13 @@
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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import {
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";
@@ -17,19 +20,27 @@ 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>("general"); const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false); "general"
);
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
const savedValue = localStorage.getItem("settingsPanelOpen");
// Check if the saved value exists and is "true"
return savedValue === "true";
});
// 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);
} }
} }
}, [location]); localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
}, [location, settingsPanelOpen]);
const handleSettingsItemClick = (item: string) => { const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item); setActiveSettingsItem(item);
@@ -43,15 +54,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
}; };
const getMenuItemClasses = (isActive: boolean) => { const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`; return `p-2 ${
isActive
? theme === "dark"
? "bg-[#1a1a1a]"
: "bg-sidebar-accent"
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
} rounded-lg flex items-center `;
}; };
if (collapsed) { 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`} />
@@ -61,33 +84,59 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
} }
return ( return (
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}> <div
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1"> className={`flex-1 flex flex-col border-t ${
<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'}`}> theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-sidebars"
} p-4 ${collapsed ? "hidden" : ""}`}
>
<Collapsible
open={settingsPanelOpen}
onOpenChange={setSettingsPanelOpen}
className="w-full flex flex-col flex-1"
>
<CollapsibleTrigger
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
}`}
>
<div className="flex items-center"> <div className="flex items-center">
<span className="font-medium tracking-wide">{t("settingPanel")}</span> <span className="font-medium tracking-wide">
{t("settingPanel")}
</span>
</div> </div>
<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 className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} /> <ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${
settingsPanelOpen ? "rotate-180" : ""
}`}
/>
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}> <CollapsibleContent
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"> <div className="space-y-2 pr-4 cursor-pointer">
{settingsMenuItems.map((item) => ( {settingsMenuItems.map((item) => (
<div <div
key={item.id} key={item.id}
className={getMenuItemClasses(activeSettingsItem === item.id)} className={getMenuItemClasses(
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-sm">{t(item.translationKey)}</span> <span className="text-base">{t(item.translationKey)}</span>
</div> </div>
))} ))}
</div> </div>
@@ -97,4 +146,4 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
</Collapsible> </Collapsible>
</div> </div>
); );
}; };
@@ -1,4 +1,3 @@
import React from "react"; import React from "react";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
@@ -10,15 +9,17 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}> <div
<div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2"> className={`p-[15.7px] ${
<img theme === "dark"
src="/favicon_sidebar.ico" ? "border-[#1e1e1e] bg-black"
alt="CheckCle" : "border-sidebar-border bg-white"
className="h-6 w-6" } border-b flex items-center ${collapsed ? "justify-center" : ""}`}
/> >
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
</div> </div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>} {!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div> </div>
); );
}; };
@@ -1,14 +1,17 @@
import { useState } from 'react'; import { useState } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage'; import {
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog'; useOperationalPages,
import { EditOperationalPageDialog } from './EditOperationalPageDialog'; useDeleteOperationalPage,
import { OperationalPageCard } from './OperationalPageCard'; } from "@/hooks/useOperationalPage";
import { Card, CardContent } from '@/components/ui/card'; import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
import { Button } from '@/components/ui/button'; import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
import { OperationalPageRecord } from '@/types/operational.types'; import { OperationalPageCard } from "./OperationalPageCard";
import { Activity, Plus, RefreshCw } from 'lucide-react'; import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from '@/components/ui/skeleton'; import { Button } from "@/components/ui/button";
import { OperationalPageRecord } from "@/types/operational.types";
import { Activity, Plus, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -18,17 +21,26 @@ 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 { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages(); const {
data: pages,
isLoading,
error,
refetch,
isRefetching,
} = useOperationalPages();
const deleteMutation = useDeleteOperationalPage(); const deleteMutation = useDeleteOperationalPage();
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null); const [editingPage, setEditingPage] = useState<OperationalPageRecord | 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] = useState<OperationalPageRecord | null>(null); const [pageToDelete, setPageToDelete] =
useState<OperationalPageRecord | null>(null);
const handleEdit = (page: OperationalPageRecord) => { const handleEdit = (page: OperationalPageRecord) => {
setEditingPage(page); setEditingPage(page);
@@ -37,10 +49,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");
} }
}; };
@@ -56,7 +68,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);
} }
} }
}; };
@@ -68,7 +80,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">Failed to load operational pages</h3> <h3 className="text-lg font-semibold mb-2">
Failed to load operational pages
</h3>
<p className="text-muted-foreground mb-4"> <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>
@@ -82,16 +96,18 @@ export const OperationalPageContent = () => {
} }
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="w-full 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">Operational Pages</h1> <h1 className="text-3xl font-bold tracking-tight mb-2">
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>
</div> </div>
<div className="flex items-center gap-2 mt-4 sm:mt-0"> <div className="flex items-center gap-2 mt-4 sm:mt-0">
<Button <Button
variant="outline" variant="outline"
@@ -99,7 +115,9 @@ export const OperationalPageContent = () => {
onClick={() => refetch()} onClick={() => refetch()}
disabled={isRefetching} disabled={isRefetching}
> >
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} /> <RefreshCw
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
/>
Refresh Refresh
</Button> </Button>
<CreateOperationalPageDialog /> <CreateOperationalPageDialog />
@@ -135,9 +153,12 @@ 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">No operational pages found</h3> <h3 className="text-lg font-semibold mb-2">
No operational pages found
</h3>
<p className="text-muted-foreground mb-6"> <p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your services and communicate status to your users. Create your first operational page to start monitoring your
services and communicate status to your users.
</p> </p>
<CreateOperationalPageDialog /> <CreateOperationalPageDialog />
</CardContent> </CardContent>
@@ -167,12 +188,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>
@@ -191,7 +212,9 @@ 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 action cannot be undone and will permanently remove the operational page and all its components. Are you sure you want to delete "{pageToDelete?.title}"? This
action cannot be undone and will permanently remove the
operational page and all its components.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -201,11 +224,11 @@ 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>
</AlertDialog> </AlertDialog>
</div> </div>
); );
}; };
@@ -1,8 +1,13 @@
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react"; import { 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";
@@ -15,24 +20,29 @@ export const RegionalMonitoringContent = () => {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: regionalServices = [], isLoading, error } = useQuery({ const {
queryKey: ['regional-services'], data: regionalServices = [],
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: "The regional monitoring agent has been successfully configured.", description:
"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.",
@@ -46,14 +56,18 @@ export const RegionalMonitoringContent = () => {
} }
}; };
const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length; const onlineAgents = regionalServices.filter(
(agent) => agent.connection === "online"
).length;
const totalAgents = regionalServices.length; 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">Regional Monitoring</h1> <h1 className="text-3xl font-bold tracking-tight">
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>
@@ -85,23 +99,25 @@ 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">{onlineAgents}</div> <div className="text-2xl font-bold text-green-600">
<p className="text-xs text-muted-foreground"> {onlineAgents}
Currently connected </div>
</p> <p className="text-xs text-muted-foreground">Currently connected</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">Offline Agents</CardTitle> <CardTitle className="text-sm font-medium">
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">{totalAgents - onlineAgents}</div> <div className="text-2xl font-bold text-red-600">
<p className="text-xs text-muted-foreground"> {totalAgents - onlineAgents}
Disconnected agents </div>
</p> <p className="text-xs text-muted-foreground">Disconnected agents</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -109,7 +125,7 @@ export const RegionalMonitoringContent = () => {
{/* Agents List */} {/* Agents List */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-xl font-semibold">Regional Agents</h2> <h2 className="text-xl font-semibold">Regional Agents</h2>
{isLoading ? ( {isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
@@ -133,7 +149,8 @@ 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 extend your monitoring coverage. Get started by adding your first regional monitoring agent to
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" />
@@ -144,9 +161,9 @@ export const RegionalMonitoringContent = () => {
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{regionalServices.map((agent) => ( {regionalServices.map((agent) => (
<RegionalAgentCard <RegionalAgentCard
key={agent.id} key={agent.id}
agent={agent} agent={agent}
onDelete={() => handleDeleteAgent(agent.id)} onDelete={() => handleDeleteAgent(agent.id)}
/> />
))} ))}
@@ -161,4 +178,4 @@ export const RegionalMonitoringContent = () => {
/> />
</div> </div>
); );
}; };
@@ -1,33 +1,37 @@
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 { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService'; import {
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] = useState(false); const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false); 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);
// Initialize maintenance notifications when the component mounts // Initialize maintenance notifications when the component mounts
useEffect(() => { useEffect(() => {
// console.log("Initializing maintenance notifications"); // console.log("Initializing maintenance notifications");
initMaintenanceNotifications(); initMaintenanceNotifications();
// Clean up when the component unmounts // Clean up when the component unmounts
return () => { return () => {
// console.log("Cleaning up maintenance notifications"); // console.log("Cleaning up maintenance notifications");
stopMaintenanceNotifications(); stopMaintenanceNotifications();
}; };
}, []); }, []);
@@ -43,65 +47,67 @@ export const ScheduleIncidentContent = () => {
const handleMaintenanceCreated = () => { const handleMaintenanceCreated = () => {
// Refresh data by incrementing the refresh trigger // Refresh data by incrementing the refresh trigger
const newTriggerValue = refreshTrigger + 1; const newTriggerValue = refreshTrigger + 1;
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue); // console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
setRefreshTrigger(newTriggerValue); setRefreshTrigger(newTriggerValue);
// Show success toast // Show success toast
toast({ toast({
title: t('success'), title: t("success"),
description: t('maintenanceCreatedSuccess'), description: t("maintenanceCreatedSuccess"),
}); });
}; };
const handleIncidentCreated = () => { const handleIncidentCreated = () => {
// Refresh data by incrementing the refresh trigger // Refresh data by incrementing the refresh trigger
const newTriggerValue = incidentRefreshTrigger + 1; const newTriggerValue = incidentRefreshTrigger + 1;
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue); // console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
setIncidentRefreshTrigger(newTriggerValue); setIncidentRefreshTrigger(newTriggerValue);
// 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 p-6"> <main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl">
<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" ? t('createMaintenanceWindow') : t('createIncident')} {activeTab === "maintenance"
? t("createMaintenanceWindow")
: t("createIncident")}
</Button> </Button>
</div> </div>
<Tabs <Tabs
defaultValue="maintenance" defaultValue="maintenance"
className="w-full" className="w-full"
onValueChange={(value) => setActiveTab(value)} onValueChange={(value) => setActiveTab(value)}
> >
<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>
<TabsContent value="maintenance" className="space-y-4"> <TabsContent value="maintenance" className="space-y-4">
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} /> <ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
</TabsContent> </TabsContent>
<TabsContent value="incidents" className="space-y-4"> <TabsContent value="incidents" className="space-y-4">
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} /> <IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
</TabsContent> </TabsContent>
@@ -109,14 +115,14 @@ export const ScheduleIncidentContent = () => {
</div> </div>
{/* Maintenance creation dialog */} {/* Maintenance creation dialog */}
<CreateMaintenanceDialog <CreateMaintenanceDialog
open={createMaintenanceDialogOpen} open={createMaintenanceDialogOpen}
onOpenChange={setCreateMaintenanceDialogOpen} onOpenChange={setCreateMaintenanceDialogOpen}
onMaintenanceCreated={handleMaintenanceCreated} onMaintenanceCreated={handleMaintenanceCreated}
/> />
{/* Incident creation dialog */} {/* Incident creation dialog */}
<CreateIncidentDialog <CreateIncidentDialog
open={createIncidentDialogOpen} open={createIncidentDialogOpen}
onOpenChange={setCreateIncidentDialogOpen} onOpenChange={setCreateIncidentDialogOpen}
onIncidentCreated={handleIncidentCreated} onIncidentCreated={handleIncidentCreated}
+265 -105
View File
@@ -1,13 +1,45 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import {
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; DropdownMenu,
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react"; DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
RefreshCw,
Search,
Eye,
Activity,
MoreHorizontal,
Pause,
Play,
Edit,
Trash2,
} from "lucide-react";
import { Server } from "@/types/server.types"; import { Server } from "@/types/server.types";
import { ServerStatusBadge } from "./ServerStatusBadge"; import { ServerStatusBadge } from "./ServerStatusBadge";
import { OSTypeIcon } from "./OSTypeIcon"; import { OSTypeIcon } from "./OSTypeIcon";
@@ -23,7 +55,11 @@ interface ServerTableProps {
onRefresh: () => void; onRefresh: () => void;
} }
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { export const ServerTable = ({
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);
@@ -34,10 +70,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const filteredServers = servers.filter(server => const filteredServers = servers.filter(
server.name.toLowerCase().includes(searchTerm.toLowerCase()) || (server) =>
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) || server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase()) server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
); );
const handleViewDetails = (serverId: string) => { const handleViewDetails = (serverId: string) => {
@@ -51,41 +88,44 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const handlePauseResume = async (server: Server) => { const handlePauseResume = async (server: Server) => {
const serverId = server.id; const serverId = server.id;
const isPaused = server.status === "paused"; const isPaused = server.status === "paused";
if (pausingServers.has(serverId)) { if (pausingServers.has(serverId)) {
return; // Already processing this server return; // Already processing this server
} }
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 ${server.name}`, description: `Monitoring ${isPaused ? "resumed" : "paused"} for ${
server.name
}`,
}); });
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`); // console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
// Refresh the server list to show updated status // 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 ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`, description: `Failed to ${
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;
@@ -108,24 +148,23 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
try { try {
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",
description: `${selectedServer.name} has been deleted successfully.`, description: `${selectedServer.name} has been deleted successfully.`,
}); });
// Refresh the server list // Refresh the server list
onRefresh(); onRefresh();
// 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({
variant: "destructive", variant: "destructive",
title: "Error", title: "Error",
@@ -136,65 +175,65 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
} }
}; };
const CustomProgressBar = ({ const CustomProgressBar = ({
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]"> <div className="space-y-2 min-w-[120px] -mt-20">
<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"> <span className="text-xs text-muted-foreground">{subtitle} </span>
{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(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`} className={`h-full bg-gradient-to-r ${getGradientColors(
type,
value
)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
style={{ width: `${Math.min(value, 100)}%` }} 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>
); );
@@ -202,7 +241,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
if (isLoading) { if (isLoading) {
return ( return (
<Card className="flex-1 flex flex-col"> <Card className="flex-1 flex flex-col ">
<CardHeader className="flex-shrink-0"> <CardHeader className="flex-shrink-0">
<CardTitle>Servers</CardTitle> <CardTitle>Servers</CardTitle>
</CardHeader> </CardHeader>
@@ -244,33 +283,113 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<p className="text-muted-foreground">No servers found</p> <p className="text-muted-foreground">No servers found</p>
</div> </div>
) : ( ) : (
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}> <div
className={`${
theme === "dark" ? "bg-gray-900" : "bg-white"
} rounded-lg border border-border shadow-sm`}
>
<Table> <Table>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}> <TableHeader
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}> className={`${
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead> theme === "dark" ? "bg-gray-800" : "bg-gray-50"
<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> <TableRow
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead> className={`${
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead> theme === "dark"
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead> ? "border-gray-700 hover:bg-gray-800"
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead> : "border-gray-200 hover:bg-gray-100"
<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 = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0; const memoryUsage =
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0; server.ram_total > 0
? (server.ram_used / server.ram_total) * 100
: 0;
const diskUsage =
server.disk_total > 0
? (server.disk_used / server.disk_total) * 100
: 0;
const isPaused = server.status === "paused"; const isPaused = server.status === "paused";
const isProcessing = pausingServers.has(server.id); const isProcessing = pausingServers.has(server.id);
return ( return (
<TableRow <TableRow
key={server.id} key={server.id}
className="hover:bg-muted/50 cursor-pointer" className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleViewDetails(server.id)} onClick={() => handleViewDetails(server.id)}
> >
@@ -285,7 +404,10 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<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 className="text-sm truncate" title={server.os_type}> <span
className="text-sm truncate"
title={server.os_type}
>
{server.os_type} {server.os_type}
</span> </span>
</div> </div>
@@ -307,7 +429,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<CustomProgressBar <CustomProgressBar
value={memoryUsage} value={memoryUsage}
label={`${memoryUsage.toFixed(1)}%`} label={`${memoryUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(server.ram_total)} subtitle={serverService.formatBytes(
server.ram_total
)}
type="memory" type="memory"
/> />
</TableCell> </TableCell>
@@ -315,12 +439,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<CustomProgressBar <CustomProgressBar
value={diskUsage} value={diskUsage}
label={`${diskUsage.toFixed(1)}%`} label={`${diskUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(server.disk_total)} subtitle={serverService.formatBytes(
server.disk_total
)}
type="disk" type="disk"
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-sm truncate" title={server.uptime}> <div
className="text-sm truncate"
title={server.uptime}
>
{server.uptime} {server.uptime}
</div> </div>
</TableCell> </TableCell>
@@ -329,10 +458,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
{new Date(server.last_checked).toLocaleString()} {new Date(server.last_checked).toLocaleString()}
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}> <TableCell
className="text-right"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}> <Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isProcessing}
>
<span className="sr-only">Open menu</span> <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" />
@@ -341,20 +477,36 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]"> <DropdownMenuContent
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}> align="end"
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 onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}> <DropdownMenuItem
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) => { e.stopPropagation(); handlePauseResume(server); }} onClick={(e) => {
e.stopPropagation();
handlePauseResume(server);
}}
disabled={isProcessing} disabled={isProcessing}
> >
{isPaused ? ( {isPaused ? (
@@ -370,12 +522,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}> <DropdownMenuItem
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) => { e.stopPropagation(); handleDelete(server); }} onClick={(e) => {
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" />
@@ -406,19 +566,19 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle> <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}> <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={confirmDelete} onClick={confirmDelete}
disabled={isDeleting} disabled={isDeleting}
@@ -25,19 +25,21 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
return start; return start;
}, [endDate, interval]); }, [endDate, interval]);
// Fetch uptime data with very aggressive caching to reduce API calls // Fetch uptime data with immediate display and background updates
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: 30000, // Data is fresh for 30 seconds staleTime: 300000, // Data stays fresh for 5 minutes - display immediately from cache
gcTime: 600000, // Keep in cache for 10 minutes gcTime: 900000, // Keep in cache for 15 minutes
refetchInterval: 60000, // 1 minute polling refetchInterval: 300000, // Reduced to 5 minute polling
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
retry: 3, retry: 1, // Reduce retries for faster failure
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), retryDelay: 1000, // Faster retry delay
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
@@ -2,13 +2,24 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import {
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 { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl"; import {
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";
@@ -21,18 +32,23 @@ 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] = useState<SSLCertificate | null>(null); const [selectedCertificate, setSelectedCertificate] =
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 { data: certificates = [], isLoading, error } = useQuery({ const {
queryKey: ['ssl-certificates'], data: 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;
} }
}, },
@@ -44,101 +60,114 @@ 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(error instanceof Error ? error.message : t('failedToAddCertificate')); toast.error(
} error instanceof Error ? error.message : t("failedToAddCertificate")
);
},
}); });
// Edit certificate mutation - Updated to include notification_id and template_id // 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.collection('ssl_certificates').update(certificate.id, updateData); const updated = await pb
.collection("ssl_certificates")
.update(certificate.id, updateData);
// After updating the settings, refresh the certificate to ensure it's up to date // 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
const refreshedCert = await checkAndUpdateCertificate(certificate.id); const refreshedCert = await checkAndUpdateCertificate(certificate.id);
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(error instanceof Error ? error.message : t('failedToUpdateCertificate')); toast.error(
} 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(error instanceof Error ? error.message : t('failedToDeleteCertificate')); toast.error(
} error instanceof Error ? error.message : t("failedToDeleteCertificate")
);
},
}); });
// Refresh certificate mutation - Updated to remove individual toast notifications // 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
}, },
onError: (error) => { onError: (error) => {
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(t('allCertificatesRefreshed').replace('{count}', result.success.toString())); toast.success(
t("allCertificatesRefreshed").replace(
"{count}",
result.success.toString()
)
);
} else { } else {
toast.info(t('someCertificatesFailed') toast.info(
.replace('{success}', result.success.toString()) t("someCertificatesFailed")
.replace('{failed}', result.failed.toString())); .replace("{success}", result.success.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) => {
@@ -166,12 +195,14 @@ 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(t('startingRefreshAll').replace('{count}', certificates.length.toString())); toast.info(
t("startingRefreshAll").replace("{count}", certificates.length.toString())
);
refreshAllMutation.mutate(); refreshAllMutation.mutate();
}; };
@@ -182,48 +213,60 @@ 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 onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}> <Button
{t('check')} onClick={() =>
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
}
>
{t("check")}
</Button> </Button>
</div> </div>
); );
} }
return ( return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0"> <main className="flex-1 flex flex-col overflow-auto bg-background pb-0 dark:bg-black p-2 ">
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 bg-white dark:bg-black p-6 rounded-xl">
<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">{t('sslDomainManagement')}</h2> <h2 className="text-2xl font-bold text-foreground">
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p> {t("sslDomainManagement")}
</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
variant="outline" variant="outline"
onClick={handleRefreshAll} onClick={handleRefreshAll}
disabled={isRefreshingAll || refreshingId !== null} disabled={isRefreshingAll || refreshingId !== null}
className="relative" className="relative"
> >
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} /> <RefreshCw
{t('refreshAll')} className={`w-4 h-4 mr-2 ${
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">
... ...
</span> </span>
)} )}
</Button> </Button>
<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('addDomain')} <Plus className="w-4 h-4 mr-2" /> {t("addDomain")}
</Button> </Button>
</div> </div>
</div> </div>
<SSLCertificateStatusCards certificates={certificates} /> <SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6"> <div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable /> <SSLCertificatesTable />
</div> </div>
@@ -232,26 +275,29 @@ 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}
onCancel={() => setIsAddDialogOpen(false)} onCancel={() => setIsAddDialogOpen(false)}
isPending={addMutation.isPending} isPending={addMutation.isPending}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{selectedCertificate && ( {selectedCertificate && (
<Dialog open={isEditDialogOpen} onOpenChange={(open) => { <Dialog
setIsEditDialogOpen(open); open={isEditDialogOpen}
if (!open) setSelectedCertificate(null); onOpenChange={(open) => {
}}> setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('editSSLCertificate')}</DialogTitle> <DialogTitle>{t("editSSLCertificate")}</DialogTitle>
</DialogHeader> </DialogHeader>
<EditSSLCertificateForm <EditSSLCertificateForm
certificate={selectedCertificate} certificate={selectedCertificate}
onSubmit={handleUpdateCertificate} onSubmit={handleUpdateCertificate}
onCancel={() => { onCancel={() => {
@@ -265,4 +311,4 @@ export const SSLDomainContent = () => {
)} )}
</main> </main>
); );
}; };
@@ -0,0 +1,14 @@
// 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;
+25 -15
View File
@@ -1,38 +1,48 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import React, { createContext, useContext, useState } from 'react'; type SideBar = boolean;
interface SidebarContextType { interface SidebarContextType {
sidebarCollapsed: boolean; sidebarCollapsed: SideBar;
setSidebarCollapsed: (collapsed: boolean) => void; setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>;
toggleSidebar: () => void; toggleSidebar: () => void;
} }
const SidebarContext = createContext<SidebarContextType | undefined>(undefined); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
interface SidebarProviderProps {
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { children: React.ReactNode;
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 => !prev); setSidebarCollapsed((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}> <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
{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;
}; };
+54 -40
View File
@@ -1,4 +1,3 @@
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";
@@ -21,61 +20,69 @@ const InstanceMonitoring = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const { sidebarCollapsed, toggleSidebar } = useSidebar(); const { sidebarCollapsed, toggleSidebar } = useSidebar();
const navigate = useNavigate(); const navigate = useNavigate();
const [stats, setStats] = useState<ServerStats>({ const [stats, setStats] = useState<ServerStats>({
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 { data: servers = [], isLoading, error, refetch } = useQuery({ const {
queryKey: ['servers'], data: 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(() => {
if (servers.length > 0) { if (servers.length > 0) {
serverService.getServerStats(servers).then(setStats); serverService.getServerStats(servers).then(setStats);
} }
}, [servers]); }, [servers]);
const handleRefresh = () => { const handleRefresh = () => {
refetch(); refetch();
}; };
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
navigate('/login'); navigate("/login");
}; };
const handleAgentAdded = () => { const handleAgentAdded = () => {
refetch(); refetch();
}; };
if (error) { if (error) {
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6"> <main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6 ">
<div className="text-center max-w-md w-full"> <div className="text-center max-w-md w-full">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2> <h2 className="text-xl sm:text-2xl font-bold mb-4">
Error loading servers
</h2>
<p className="text-muted-foreground mb-4 text-sm sm:text-base"> <p className="text-muted-foreground mb-4 text-sm sm:text-base">
Unable to fetch server data. Please check your connection and try again. Unable to fetch server data. Please check your connection and
try again.
</p> </p>
<button <button
onClick={handleRefresh} onClick={handleRefresh}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base" className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
> >
Retry Retry
@@ -86,22 +93,22 @@ const InstanceMonitoring = () => {
</div> </div>
); );
} }
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground ">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0 ">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto dark:bg-black p-2 shadow">
<div className="p-4 sm:p-6 lg:p-8 space-y-6"> <div className="p-4 sm:p-2 lg:p-2 space-y-6 h-full bg-white dark:bg-black p-4 rounded-xl">
{/* Header Section */} {/* Header Section */}
<div> <div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-2 ">
<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
@@ -110,7 +117,10 @@ 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 onClick={() => setAddDialogOpen(true)} className="flex-shrink-0"> <Button
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>
@@ -118,13 +128,17 @@ const InstanceMonitoring = () => {
</div> </div>
{/* Stats Cards Section */} {/* Stats Cards Section */}
<div> <div className="p-2 ">
<ServerStatsCards stats={stats} /> <ServerStatsCards stats={stats} />
</div> </div>
{/* Server Table Section */} {/* Server Table Section */}
<div> <div className="p-3">
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} /> <ServerTable
servers={servers}
isLoading={isLoading}
onRefresh={handleRefresh}
/>
</div> </div>
</div> </div>
</main> </main>
@@ -139,4 +153,4 @@ const InstanceMonitoring = () => {
); );
}; };
export default InstanceMonitoring; export default InstanceMonitoring;
+46 -36
View File
@@ -1,4 +1,3 @@
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";
@@ -7,32 +6,41 @@ 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 { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService"; import {
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";
const SslDomain = () => { const SslDomain = () => {
// Get language context for translations // Get language context for translations
const { t } = useLanguage(); const { t } = useLanguage();
// Use shared sidebar state // Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar(); const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user // Get current user
const currentUser = authService.getCurrentUser(); const currentUser = authService.getCurrentUser();
const navigate = useNavigate(); const navigate = useNavigate();
// Fetch SSL certificates with error handling and debugging // Fetch SSL certificates with error handling and debugging
const { data: certificates = [], isLoading, error, refetch } = useQuery({ const {
queryKey: ['ssl-certificates'], data: 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 {
const result = await fetchSSLCertificates(); const result = await fetchSSLCertificates();
// console.log("SSL certificates fetch successful, count:", result.length); // console.log("SSL certificates fetch successful, count:", result.length);
return result; return result;
} catch (err) { } catch (err) {
// console.error("Error fetching SSL certificates from page:", err); // console.error("Error fetching SSL certificates from page:", err);
throw err; throw err;
} }
}, },
@@ -40,23 +48,23 @@ const SslDomain = () => {
refetchInterval: 300000, // Refresh every 5 minutes refetchInterval: 300000, // Refresh every 5 minutes
retry: 3, // Retry failed requests 3 times retry: 3, // Retry failed requests 3 times
}); });
// Check all SSL certificates once per day // Check all SSL certificates once per day
useEffect(() => { useEffect(() => {
const checkCertificates = async () => { const checkCertificates = async () => {
// Check if we should run daily check // Check if we should run daily check
if (shouldRunDailyCheck()) { if (shouldRunDailyCheck()) {
// console.log("Running daily SSL certificate check..."); // console.log("Running daily SSL certificate check...");
await checkAllCertificatesAndNotify(); await checkAllCertificatesAndNotify();
// Refresh certificate list after daily check // Refresh certificate list after daily check
refetch(); refetch();
} }
}; };
// Run check when component mounts // Run check when component mounts
checkCertificates(); checkCertificates();
}, [refetch]); }, [refetch]);
// Handle logout // Handle logout
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
@@ -69,11 +77,11 @@ const SslDomain = () => {
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<LoadingState /> <LoadingState />
</div> </div>
@@ -86,23 +94,25 @@ const SslDomain = () => {
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 ">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
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">{t('failedToLoadCertificates')}</h2> <h2 className="text-xl font-bold mb-2">
{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>
@@ -114,12 +124,12 @@ const SslDomain = () => {
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 ">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed} sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
<SSLDomainContent /> <SSLDomainContent />
</div> </div>
@@ -127,4 +137,4 @@ const SslDomain = () => {
); );
}; };
export default SslDomain; export default SslDomain;
+9 -2
View File
@@ -76,7 +76,7 @@ The roadmap is divided into the following stages:
- [ ] ✅ Add ntfy push notifications - [ ] ✅ Add ntfy push notifications
### 🚀 v1.6.0 Feature Enhancements _(Target: Q3 2025)_ ### 🚀 v1.6.0 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress **Status:** ✅ Completed
#### Tentative Features: #### Tentative Features:
- [ ] ✅ Allow user to update the schema directly from the dashboard - [ ] ✅ Allow user to update the schema directly from the dashboard
- [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143) - [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143)
@@ -89,7 +89,14 @@ The roadmap is divided into the following stages:
- [ ] ✅ fix(ssl): Ensure edit form saves notification_id and template_id in DB. - [ ] ✅ fix(ssl): Ensure edit form saves notification_id and template_id in DB.
- [ ] ✅ Add Notifiarr notifications - [ ] ✅ Add Notifiarr notifications
- [ ] ✅ Add Gotify notifications - [ ] ✅ Add Gotify notifications
- [ ] 🔧 Enhance the Data retention feature - [ ] Enhance the Data retention feature
### 🚀 v1.7.0 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress
#### Tentative Features:
- [ ] 🔧 Fix for the response time dashboard filter (date/time) in the uptime detail page
- [ ] 🔧 Fix the server historical loading data
- [ ] 🔧 Improve docker agent for server monitoring - [ ] 🔧 Improve docker agent for server monitoring
- [ ] 🔧 Improve the Server Agent on Unraid - [ ] 🔧 Improve the Server Agent on Unraid
- [ ] 🔧 Server Windows Agent - [ ] 🔧 Server Windows Agent