260 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
Tola Leng 5f7c451fc0 feat: add Pushover notification channel schema 2025-08-27 17:24:02 +07:00
Tola Leng 3d7172d9aa Update Project Development Status and Progress
- feat: Implement Pushover notification
- fix(services-form): max retry attempts
2025-08-24 16:58:52 +07:00
Tola Leng 22d4597f88 Merge pull request #147 from ghotso/develop-fix_max_retries_issue
fix(services-form): make retry attempts select controlled and synced with form state
2025-08-24 13:49:31 +07:00
Tola Leng d4423e328b feat: Implement Pushover notification
- Implement Pushover notification functionality to send messages similar to Telegram. This includes adding `api_token` and `user_key` fields to the alert configuration for Pushover. Closes #135 #95
2025-08-23 20:47:46 +07:00
Tola Leng b3b64806f6 feat: Add Pushover notification channel
- Adds Pushover as a new notification channel option. Includes fields for `api_token` and `user_key` in the `alert_configurations` schema and integrates it into the channel type dropdown and form handling.
2025-08-23 20:10:03 +07:00
ghotso 001f6f8783 fix: update ServiceConfigFields to set default value for retry attempts and enhance delete migration scripts for server processes and notifications with error handling for deleting non existing collections 2025-08-22 12:10:01 +02:00
Tola Leng d01f682a83 Update Project Development Status and Progress 2025-08-22 16:44:27 +07:00
Tola Leng bce523bdf1 Merge pull request #142 from ghotso/develop-fix-incident_assigned_user
fix(incident): unify assigned user handling with fallback and UI update 🎯
2025-08-22 02:47:15 +07:00
Tola Leng 623e3d5ebf Add schema JSON file locally to allow direct updates from the UI. This schema is for CheckCle version v1.5.0. 2025-08-22 02:39:01 +07:00
Tola Leng e3a7865e40 feat: Allow user to update the schema directly from the dashboard
Includes:
- Dashboard "Update Schema" action
- Multiple notification/alert fixes and enhancements
- This allow to update the schema for the latest update of environment, this update include with multiple fixes

Closes The notification function is not available #138 Still can't enable and create notifications, And A new problems #136 Can not turn on notification (Telegram , Mail....) #120 Notification type 'slack' not supported #26 REQUEST: webhook notification #43 Notification can't be enabled (Discord) #77
2025-08-22 02:37:11 +07:00
ghotso 3c93354c7d Merge branch 'operacle:develop' into develop-fix-incident_assigned_user 2025-08-21 17:15:02 +02:00
ghotso 323ac36b65 chore: add server/pb_data/ to .gitignore 2025-08-21 16:53:56 +02:00
ghotso c437704007 chore: remove pb_data runtime files and ignore directory 2025-08-21 16:49:04 +02:00
ghotso 1953d54dc5 fix(types): refine user type definitions for assigned users in incident handling 2025-08-21 16:48:57 +02:00
ghotso b574073294 fix(incidents): update detail dialog and PDF generation to prefer assigned_users over assigned_to for assignee 2025-08-21 16:48:57 +02:00
ghotso b164602ee5 fix(pdf): render assignee using assigned_users with fallback to assigned_to 2025-08-21 16:48:57 +02:00
ghotso 4bd08efc9a fix(incidents): display assignee in BasicInfoSection via assigned_users fallback 2025-08-21 16:48:57 +02:00
ghotso 58fa09bea7 fix(incidents): show assignee in AssignmentSection via assigned_users with fallback to assigned_to 2025-08-21 16:48:57 +02:00
ghotso 7ff180bea4 fix(incidents): fetch and display assignee in detail dialog via assigned_users with fallback 2025-08-21 16:48:57 +02:00
ghotso 7b18806066 fix(incidents): show assignee in table using assigned_users with fallback to assigned_to 2025-08-21 16:48:56 +02:00
ghotso d4434a64e1 fix(types): add assigned_users to incident types for server persistence 2025-08-21 16:48:56 +02:00
ghotso 08580794da fix(incidents): ensure assignee is prefilled and persisted on edit (assigned_to → assigned_users) 2025-08-21 16:48:56 +02:00
ghotso 70fdcc9ea6 fix(incidents): include assigned_users in form submit so assignee is persisted 2025-08-21 16:48:56 +02:00
ghotso aba3120235 fix(incidents): persist assignee correctly via assigned_users
- map form field `assigned_to` to backend field `assigned_users`
- ensure assignee is stored when creating or updating incidents
- keep `assigned_to` for backward compatibility
2025-08-21 16:48:56 +02:00
Tola Leng b038d87848 Database Schema Update
- Database schema up to date with the latest features and improvements
2025-08-21 14:53:52 +07:00
Tola Leng 87588a9127 Added more notification channel (GoogleChat, ntfy, webhook) into the collections configuration 2025-08-21 02:19:44 +07:00
Tola Leng 4090fa5aef Merge pull request #143 from taking/develop
feat(i18n): Add Korean translations and update types
2025-08-21 01:46:43 +07:00
taking c04561dd99 Merge branch 'operacle:develop' into develop 2025-08-20 22:56:41 +09:00
Ubuntu 7f1dfcdd42 feat(i18n): Add translation fields for de, en, ja, km, zhcn
- Update translation index to support Korean
  `application/src/translation/index.ts`
- Update translation indexes
  `application/src/translation/{de,en,ja,km,zhcn}/index.ts`
- Update dropdown menu item in dashboard header
  `application/src/components/dashboard/Header.tsx`
2025-08-20 22:54:31 +09:00
Tola Leng 6f753f2901 Merge pull request #141 from ghotso/develop
feat(i18n): 🌐 sync German translations across incident, maintenance, services & settings
2025-08-20 20:25:44 +07:00
taking c39296cee6 feat(i18n): Add Korean translations and update types
- Added Korean (ko) translations for all modules (incident, login, maintenance, menu, service, settings, ssl)
- Added previously empty strings in `application/src/translations/types`
- Followed existing style and naming conventions
2025-08-19 14:54:51 +09:00
ghotso a1a8a8443e feat(i18n): 🌐 add missing German translations for settings module
Added missing German translation keys to align with the English settings module:
- addUser
- permissionNotice
- permissionNoticeAddUser
- loadingSettings
- loadingSettingsError

Ensured all existing German translations remain unchanged. 🚀
2025-08-18 21:26:11 +02:00
ghotso f866d36db4 feat(i18n): 🌐 add missing German translations for services module
Added missing German translation keys to align with the English services module.
Also imported section comments from the English file for better structure.

Now the German translation file has full feature parity with English 🚀
2025-08-18 21:20:47 +02:00
ghotso 98ec911281 feat(i18n): 🌐 add missing German translations for incident module
Added the missing German translation keys to align with the English incident module:
- enterIncidentTitle
- enterIncidentDescription
- enterServiceId
- selectAssignedUser
- noAssignedUser
- affectedSystems
- enterAffectedSystems
- separateSystemsWithComma

This ensures feature parity between the English and German translation files.
2025-08-18 21:15:04 +02:00
ghotso 046db61326 feat(i18n): 🌐 add missing German translations for maintenance module
Added the missing German translation keys to align with the English maintenance module.

Now the German translation file is complete and feature-parity with English 🌟
2025-08-18 21:14:22 +02:00
Tola Leng 3c00d0933e Merge pull request #140 from ghotso/develop
Added missing English translations to incidents.ts and maintenance.ts
2025-08-19 01:08:54 +07:00
ghotso 8e013f0b0b Added missing EN translations to maintenance.ts
Added missing translations
2025-08-18 19:04:20 +02:00
ghotso f686134525 Added missing EN translations in incident.ts
Added missing translations
2025-08-18 18:56:17 +02:00
ghotso 98d33a609e Update translations in maintenance.ts
Added missing trranslations
2025-08-18 18:49:21 +02:00
Tola Leng d33244956e Merge pull request #137 from sqkkyzx/develop
improve i18n and add new translations
2025-08-18 21:06:03 +07:00
YiZixuan 572c572c14 refactor(i18n): improvements in Chinese translation 2025-08-17 23:22:08 +08:00
YiZixuan f9743407a4 refactor(i18n): Add internationalization support for the dashboard service status card
- Add new translation items to multiple language files, including Chinese and English
- Implement internationalization support in the StatusCards component
2025-08-17 23:21:39 +08:00
YiZixuan fa3ad9b2e1 refactor(add-service): Refactor ServiceForm.tsx to support internationalization
- Extract ServiceForm.tsx validation logic into the custom hook useServiceSchema
- Add internationalization support and move error messages to translation files
2025-08-17 23:13:25 +08:00
YiZixuan 6bf75b2bf7 refactor(i18n): Extracted multiple fields in the Service component for translation.
- Extracted multiple fields in the Service component for translation.
- Optimized the structure of the translation file, categorizing it by functional modules.
2025-08-17 15:19:01 +08:00
YiZixuan b068c0cd51 refactor(i18n): improve translation handling and add English fallback
- Add English as the default fallback language; automatically use English when a translation for the current language is missing.
2025-08-16 16:21:28 +08:00
YiZixuan 92617038f0 refactor(i18n): Improve multilingual support 2025-08-16 15:59:00 +08:00
YiZixuan 40488a5a53 feat(i18n): improve i18n copy and add new translations
- Add new translation entries
- Fix several language files
2025-08-16 15:25:35 +08:00
Tola Leng 35fe0b9071 feat: Implement NTFY notification functionality
- Implement NTFY push notification functionality to send messages to ntfy toptics.
2025-08-15 17:17:29 +07:00
Tola Leng 3bc1fd7c02 feat: Add NTFY notification channel
- Adds NTFY as a new option to the notification channel type dropdown
- This includes updating the form schema and related components to support the ntfy_endpoint field for NTFY configurations.
2025-08-15 16:47:26 +07:00
Tola Leng ee4ce50193 📜 Correct the DigitalOcean logo URL 2025-08-15 14:07:57 +07:00
Tola Leng 67a0d1d7f4 📜 Adds some default styling to table cells. 2025-08-15 14:00:01 +07:00
Tola Leng f006df3b02 📜 Use a simple HTML table for image layout fo sponsorship logo 2025-08-15 13:56:16 +07:00
Tola Leng 5234303565 📌 Update on Project Development Status and Progress 2025-08-15 13:48:51 +07:00
Tola Leng 4ff0457966 📜 Wrap display logo in a flex container 2025-08-15 13:45:31 +07:00
Tola Leng 7a00d8828e 💼 Update on Project Development Status and Progress 2025-08-14 22:56:52 +07:00
Tola Leng 6193ad8b4f 📧 Added Powerful Notification System Image and Sponsorship 2025-08-14 22:54:02 +07:00
Tola Leng 41c387a301 Removed the insert Webhook Payload Template in the Notification Channel Dialog. 2025-08-14 21:00:15 +07:00
Tola Leng 933e70a3f6 feat: Implement comprehensive multi-channel notification system with templating and SSL monitoring
- Add robust notification system supporting 7+ communication channels with intelligent message templating, resource monitoring, and SSL certificate alerts. Notification Channels: (Telegram, Discord, Slack, Signal, Email, Google Chat, Webhooks).

- This notification system provides enterprise-grade alerting capabilities with extensive customization options and multi-channel redundancy for critical service monitoring.
2025-08-14 20:50:31 +07:00
Tola Leng 58242c33f8 Refactor Webhook to use alert_configurations and Add Signal API endpoint field
- This field will be used to specify the API endpoint for Signal notifications.
- This change aligns the frontend with the backend's data structure for webhooks.
2025-08-14 04:54:15 +07:00
Tola Leng 47a38d3c64 Refactor Webhook fields
- Condense webhook fields in the notification channel form to display only essential information, mirroring the approach in webhook service.
2025-08-14 03:39:47 +07:00
Tola Leng 07c101617c feat: Add SMTP password field for email
- Add an input field for SMTP password in the "Add Notification Channel" dialog. This field will be used for SMTP authentication.
2025-08-13 23:08:10 +07:00
Tola Leng 37bb7e25b6 Fix: Display existing SSL notification and template data
- This change ensures that users can see and manage their existing SSL configurations.
2025-08-13 21:42:48 +07:00
Tola Leng a4e819d220 Fix: Preserve notification IDs when disabling notifications
- Allowing notifications to be re-enabled correctly without losing previously configured
2025-08-13 17:20:24 +07:00
Tola Leng 7b01cffb64 Merge pull request #128 from sqkkyzx/develop
feat(translations): Add Simplified Chinese translation
2025-08-13 14:42:18 +07:00
YiZixuan 6479ea50a5 docs: Add Chinese documentation and update the language selection list
- Add a README_zhcn.md file in the docs directory, which contains the Chinese documentation content.
- Add a link to the Chinese language option in README.md and READMEs in other languages
2025-08-13 00:48:37 +08:00
YiZixuan 9596f53742 feat(translations): Add German and Japanese translation fields
- Added the "german" and "japanese" fields to the common.ts file
2025-08-13 00:22:44 +08:00
YiZixuan f15a00772e feat(translations): Add Simplified Chinese translation
- Add simplified Chinese translation files
- Add the Simplified Chinese option in the language selection
- Update the translation index to support Simplified Chinese
2025-08-13 00:05:16 +08:00
Tola Leng d4636d5436 feat: Add error_message placeholder for uptime notifications
- Add support for the `${error_message}` placeholder in uptime service notifications
2025-08-12 22:26:51 +07:00
Tola Leng 37bc86e9d8 feat: Enhance uptime notification templates
- Improve Telegram notification messages for uptime services by adding support for new placeholders.
2025-08-12 22:23:31 +07:00
Tola Leng 34d3e81154 Removed entrypoint script with hidden pocketbase 2025-08-12 15:12:35 +07:00
Tola Leng e70b23cbe5 Add placeholders to Service Uptime templates
- Adds support for new placeholders in the Service Uptime Monitoring alert templates. The following placeholders are now available:
-- ${ip_host}: The IP address or hostname of the service.
-- ${service_type}: The type of the service (e.g., HTTP, PING, TCP, DNS).
-- ${port}: The port the service is running on.
-- ${domain}: The domain name of the service.
-- ${region_agent}: The regional agent monitoring the service.
2025-08-12 14:49:13 +07:00
Tola Leng 36dfd8a52c Add restore messages to server notification templates
- Add new fields for restore messages to the Server Template Dialog to match the DB schema.
2025-08-11 23:09:43 +07:00
Tola Leng 86503b7160 Fix: Prevent clearing notification fields
- This fix ensures that when the notification status is toggled off, notification_channels, threshold_id, and template_id are no longer cleared or emptied.
2025-08-11 17:43:55 +07:00
Tola Leng aecf16ba40 Fix: Correctly update notification status
- This fix ensures that when the notification status is enabled. Resolves the issue where the status was incorrectly shown as false in PB.
2025-08-11 17:33:08 +07:00
Tola Leng 2ca7c35c69 Update Translate SSL Certificate Details
- Translate untranslated text in the SSL Certificate Details dialog. The following keys have been translated:
- certificateDetails
- subjectAlternativeNames
- monitoringConfiguration
- technicalInformation
2025-08-10 16:38:15 +07:00
Tola Leng 81604cb736 Merge pull request #119 from gnsworks/develop
Update Japanese translation
2025-08-10 16:21:29 +07:00
Tola Leng 9f9d773256 Fix: Wrap Subject Alternative Names
- Wrap the Subject Alternative Names (SANs) in the SSL detail dialog to ensure they break onto new lines when there are multiple entries, improving readability.
2025-08-10 16:07:04 +07:00
Tola Leng 92e5f3f074 feat: Add Server Threshold Tab
- Add a new tab for "Server Threshold" to the Alert Templates dashboard, to allow users to manage server threshold configurations.
2025-08-09 20:06:40 +07:00
gnsworks 2f12f8f805 Add docs/DEVELOPMENT_ROADMAP_ja.md 2025-08-09 13:16:22 +09:00
gnsworks c6e7e243e6 Update : japanese documents translation 2025-08-09 11:57:14 +09:00
gnsworks 074fc18ee1 Merge branch 'operacle:develop' into develop 2025-08-09 08:54:50 +09:00
Tola Leng 34dccb763d 📝 Added sponsoring the OSS documentation site 2025-08-08 21:24:39 +07:00
Tola Leng cc6abf58ac Remove Quick Actions button and dialog
- Remove the QuickActionsDialog component and its associated file from the dashboard.
2025-08-08 15:39:57 +07:00
Tola Leng 269240c6c9 Add icons to notification channels
- Display icons for each notification channel type in the dialog and dropdown.
2025-08-05 22:27:37 +07:00
Tola Leng 7856aff98e Updated Server Installation Tips 2025-08-05 22:01:20 +07:00
Tola Leng 99d67deb10 Add Google Chat webhook to notification channels
- Added Google Chat  as a new channel type option in the Notification Channel
- Add Google Chat tab to notifications
- Fix: Ensure Google Chat and Email save record to PB
2025-08-05 21:35:56 +07:00
Tola Leng 96bf456338 feat: Add & Enhance Notification Channel and Alert Template to Add/Edit Domain Form
- Add multi-select for notification channels and an alert template field to the Add/Edit SSL Certificate dialog
2025-08-04 21:59:04 +07:00
Tola Leng f4cc96733c Refactor threshold update logic
- Remove the separate "Update Thresholds" button and integrate its functionality into the main "Update Server" button.
2025-08-04 21:26:19 +07:00
Tola Leng da4678570f Fixed: Remove [Default] alert template option
- The Alert Template field in the Service dialog will now correctly fetch data from service_notification_templates
2025-08-04 21:14:52 +07:00
gnsworks 881d745776 Merge branch 'operacle:develop' into develop 2025-08-03 16:03:21 +09:00
Tola Leng b4dc510611 Add webhook payload template
- Add webhook notification channel configuration. Allow users to define custom payload structures for webhook notifications.
2025-08-02 20:37:51 +07:00
Tola Leng 914f1aba60 feat: Support multiple notification channels
- Update the Notification Settings dashboard to allow users to select multiple notification channels for alerts.
- Add webhook to notification channels: Add webhook as a selectable option in the notification channel type dropdown.
- Implement template type selection in the Add Template dialog. Based on the selected type.
2025-08-02 16:53:15 +07:00
Tola Leng f7cf2fcc20 Fix: Set SSL initialize check with the current time. Closes: #100
- When adding a new SSL certificate, initialize with the current time. This ensures that the SSL check is triggered shortly after the certificate is added to the monitoring system.
2025-07-31 17:28:41 +07:00
Tola Leng 50b4d1dfb0 Refactor: Reduce verbose logging output in Regional Script
- Reduce verbose logging in install-regional-agent.sh to provide a more minimal output while retaining all core functionality.
2025-07-31 16:19:16 +07:00
Tola Leng 8bb78e68ea Disable verbose logging
- Disable log console output in file to prevent logs from being printed to the console.
2025-07-31 14:57:16 +07:00
Tola Leng 205bedfba5 Updated project development status 2025-07-31 02:58:02 +07:00
Tola Leng 1fac88dcc9 feat: add docker compose for server docker agent 2025-07-31 02:52:57 +07:00
Tola Leng 44bc2bc1e5 feat: Create server-docker-agent.sh script
- This script will automate the process of running the monitoring agent Docker container. It will take environment variables as input and construct the `docker run` command with the provided values, including volumes and ports, to ensure a seamless one-click installation experience.
2025-07-31 02:46:27 +07:00
Tola Leng bc06a24adb feat: Add Docker One-Click Install Tab
- Adds a new "Docker One-Click" tab to the server dashboard and create new server dialog. This tab provides a one-click installation script for the Docker monitoring agent, dynamically populating it with server-specific details.
2025-07-31 02:44:51 +07:00
Tola Leng 952eb3d8d7 Update script URL to deploy from CDN 2025-07-29 21:19:38 +07:00
Tola Leng 1d8b5172c5 Added the OS logo icon for Arch Linux 2025-07-29 20:54:54 +07:00
Tola Leng ff4938533c Fix: Add tar.gz fallback for unsupported systems
- The script has been updated to include a fallback mechanism for systems that do not have `dpkg` or `rpm` package managers. This ensures that the agent can be installed on a wider range of Linux distributions, such as Arch Linux, by automatically using the `tar.gz` package when native package managers are not detected.
2025-07-29 20:54:20 +07:00
gnsworks 384920e952 Merge branch 'operacle:develop' into develop 2025-07-29 09:03:10 +09:00
Tola Leng cad092bc6b 📖 Add sections for Project Status and Sponsorship to the README. 2025-07-28 02:12:49 +07:00
Tola Leng be57688a5e 📖 Update X (twitter) Account 2025-07-28 01:27:29 +07:00
Tola Leng e2f41fdef1 feat: Add pagination to SSL table
- Implement pagination for the SSL dashboard table, includes adding a dropdown for selecting the number of records per page (10, 30, 50).
2025-07-27 20:07:59 +07:00
Tola Leng c329425ac1 Refactor the project developement roadmap 2025-07-27 19:22:03 +07:00
Tola Leng fd3b959966 📖 Updated README - Refactor the project developement roadmap 2025-07-27 19:21:32 +07:00
gnsworks 4157ee11cc Merge branch 'operacle:develop' into develop 2025-07-25 21:20:34 +09:00
Tola Leng 86e707803e Fix: Improve dropdown theme support
- Update dropdown themes by ensuring its background color adapts correctly.
2025-07-25 17:42:25 +07:00
Tola Leng 8d279aa0da Fix: Resolve invalid DOM nesting in OperationalPageCard
- Resolved the `validateDOMNesting` warning. The `Badge` will be rendered directly within a `<div>` or other appropriate block-level element.
2025-07-25 17:37:29 +07:00
Tola Leng 07dc10a422 Remove Quick Add Template
- This section provided pre-defined component types for quick addition, but is no longer needed.
2025-07-25 17:34:37 +07:00
Tola Leng e9cedef443 Disabled console log for production 2025-07-25 16:59:49 +07:00
Tola Leng c827151521 Merge pull request #91 from michaelortnerit/develop
[IMP] Added view details (for the names) on the Server Instances and …
2025-07-25 01:31:20 +07:00
Michael Ortner 9d5fb72708 [IMP] Added view details (for the names) on the Server Instances and SSL Views 2025-07-24 07:59:50 +02:00
Tola Leng 2bc508a230 Updated GitHub issue templates for Bug Report, Help Request, and Translation Request
- Bug Report template with detailed reproduction steps, logs, and checklist
- Ask for Help template to guide users in submitting clear questions
- Translation Request template to facilitate multilingual contributions
2025-07-23 22:55:04 +07:00
Tola Leng fda91aedb9 Fix: Display new service immediately
- Ensure that newly created services are immediately visible in the services table list on the Uptime Monitoring dashboard, without requiring a manual refresh or a delay.
2025-07-23 22:13:20 +07:00
Tola Leng 4ff92b4c4d Updated server metric field 2025-07-23 21:55:23 +07:00
Tola Leng fc568891d5 📖 Updated README - For project status and url image 2025-07-23 21:53:31 +07:00
Tola Leng 57f815e29e Updated server agent automate bash script 2025-07-23 21:52:06 +07:00
Tola Leng 9f704bb1ad Added Reddit Community 2025-07-22 20:11:11 +07:00
Tola Leng 0cba09f53d Updated Credentail for Demo [Docs]: Demo Login #74 2025-07-22 15:46:44 +07:00
Tola Leng 2d2122d2e5 Improved the Server feature History Data 2025-07-21 22:18:27 +07:00
Tola Leng d17a70dfa8 Updated server schema for migration 2025-07-21 16:38:57 +07:00
Tola Leng 3f3ce9122d Adjust the size of the server title header. 2025-07-21 16:35:28 +07:00
Tola Leng 60960cb3e0 Refactor: Update server progress bar style
- Change the server progress bar style in the Instance Monitoring and server table.
2025-07-21 15:51:54 +07:00
Tola Leng 50c742d82d Added new favicon logo file 2025-07-21 15:26:33 +07:00
Tola Leng d0ecaaaf8f Refactor: Improve chart color and style
- Fixed chart colors to be fixed with random colors.
- Added another chart style.
2025-07-21 01:29:24 +07:00
Tola Leng ff19d46391 Updated favicon logo file 2025-07-21 00:57:47 +07:00
Tola Leng e5afe276ee Feat: Add Heatmap button to service detail page
- Adds a "Heatmap" button to the service detail page, displaying a monthly health heatmap of the service, with statuses color-coded as green, red, and warning (yellow/orange).
2025-07-21 00:57:04 +07:00
Tola Leng c098ecb6bf Fix: Apply server table background to SSL table
- Apply the same background color and styling from the server table to the SSL table for visual consistency.
2025-07-19 15:37:00 +07:00
Tola Leng dc586dc15f Fix: Server Historical Performance chart improvement
Fix: Improve sidebar collapse/expand performance
2025-07-19 15:23:28 +07:00
Tola Leng d40d5898c0 Fix SSL frontend notifications
- The frontend no longer sends notifications. The backend is now responsible for sending SSL notifications.
2025-07-19 15:14:04 +07:00
Tola Leng 63d63a908a Refactor: Make instance monitoring page responsive
- Make the Instance Monitoring page and Server Table List fully responsive across the whole page.
2025-07-18 22:01:26 +07:00
Tola Leng 868832f5ba feat: Add create instance monitoring button
- Adds a button to the Instance Monitoring dashboard to create server monitoring instances
- Include the one-click install command in the Instance Monitoring form
- Display OS list for select with images and names.
2025-07-18 21:57:56 +07:00
Tola Leng c1c94115e0 Feat: Added Operating System Logo for Server Agent 2025-07-18 21:52:16 +07:00
Tola Leng e388912cee Feat: added bash script for one click server agent montioring 2025-07-18 21:51:25 +07:00
Tola Leng 6c227f496e Remove individual toast notifications
- During SSL certificate refresh and display a single summary notification after the "Refresh All" action.
2025-07-18 14:03:32 +07:00
Tola Leng 0cad374616 Fix server delete function and Implement pause/resume functionality
- Implement the server delete functionality in the server table and dropdown menu.
- Implement pause and resume functionality for server monitoring in the server table dropdown menu.
2025-07-17 23:03:40 +07:00
Tola Leng 6906a65952 Fix: Improve uptime monitoring dashboard performance
- Reduce refetch intervals, increase stale times, and optimize retry behavior for uptime service and server metrics.
2025-07-17 17:26:17 +07:00
Tola Leng b9cdff37d5 Merge pull request #73 from lyker189/develop
Updated Incident Management Overview Cards linear-gradient
2025-07-17 17:15:59 +07:00
aa123456 88877a9fc0 Updated Incident Management Overview Cards linear-gradient 2025-07-17 16:36:00 +07:00
Tola Leng 2de3a17967 Added issue template icon 2025-07-15 22:32:02 +07:00
Tola Leng b4a12a46df Added config file blank issues 2025-07-15 22:27:20 +07:00
Tola Leng e4284afb09 Remove subtitle under About System page 2025-07-15 22:13:46 +07:00
Tola Leng 664e2c8ecf Added ISSUE_TEMPLATE 2025-07-15 22:13:07 +07:00
Tola Leng 7c08c8bc66 Added CODE OF CONDUCT 2025-07-15 22:12:21 +07:00
gnsworks 88b4803f52 Merge branch 'operacle:develop' into develop 2025-07-15 21:23:16 +09:00
Tola Leng fd7035965f Disabled console debug log Statements from the production build output 2025-07-15 17:36:12 +07:00
Tola Leng debf35703b Update email test and INCIDENT template 2025-07-15 17:34:04 +07:00
Tola Leng 213446422e Fix: Ensure all SSL Certs records are checked
- Addressing the issue where only a limited number (30) were being processed due to pagination. The fixed now fetches all records to ensure comprehensive SSL monitoring.
2025-07-15 17:16:09 +07:00
Tola Leng 79beb20b65 Fix: Ensure all uptime service records are checked
- Addressing the issue where only a limited number (30) were being processed due to pagination. The fixed now fetches all records to ensure comprehensive monitoring.
2025-07-15 17:15:08 +07:00
Tola Leng f235043417 Fix: Display all SSL up to 200 records
- The SSL certs table in the SSL & Domain dashboard was not displaying all certs records
2025-07-15 16:21:56 +07:00
Tola Leng 6512d4974a feat: Add pagination to service table
- Add pagination to the service table in the uptime monitoring dashboard, allowing display of 10 records per page by default. Also add a dropdown to select the number of records to display (10, 30, 50).
2025-07-15 15:26:23 +07:00
Tola Leng 73f2566d78 Fix: Display all service records
- The service table in the uptime monitoring dashboard was not displaying all service records
2025-07-15 15:24:23 +07:00
Tola Leng 46077e8deb Removed unused script line 2025-07-14 21:49:33 +07:00
Tola Leng d72d6fb009 Updated DB Data and Schema Migration 2025-07-14 21:48:39 +07:00
gnsworks c6ed816c04 Update Japanese docs 2025-07-14 20:32:08 +09:00
Tola Leng 0ba3b3182b Disable SPONSORSHIP 2025-07-14 15:46:12 +07:00
Tola Leng 24c5a9957f Fix: Prevent infinite re-renders.
- Addressed an issue where the component was re-rendering excessively, leading to performance problems.
2025-07-14 15:45:38 +07:00
Tola Leng cb258d72c3 Disable debug console logs for production 2025-07-14 15:45:05 +07:00
Tola Leng 0941250867 Refactor Split README and CONTRIBUTING into smaller components with support for multi languages 2025-07-13 18:32:45 +07:00
Tola Leng 7ef4d27148 Merge pull request #66 from gnsworks/develop
Add Japanese translation
2025-07-13 15:38:28 +07:00
gnsworks 2621544b24 Add Japanese translation 2025-07-12 10:31:03 +09:00
gnsworks 9fd977ed15 Add CONTRIBUTING_ja.md 2025-07-12 10:07:54 +09:00
gnsworks 1b40a1b663 Add README_ja.md 2025-07-12 10:06:50 +09:00
Tola Leng 53d8b110c9 Update .gitignore with .DS_Store removed 2025-07-11 20:12:20 +07:00
Tola Leng a013f9170a feat: Allow multiple regional agents
- Allow multiple selections for regional agents in the service dialog form, similar to notification channels.
2025-07-11 17:59:03 +07:00
Tola Leng 5daea8138c Change Default monitoring fallback source value (uptime service) 2025-07-11 17:58:46 +07:00
Tola Leng 59dcdcb5df Fix: Persist multiple notification channels
- The notification channels field in the service dialog form was not saving or applying multiple selections
2025-07-11 17:57:32 +07:00
Tola Leng 35e161e40d 🔐 docs(security): update Security Policy to reflect TypeScript and Go stack
- Clarify that CheckCle is built with both TypeScript and Go
- Add mention of Go-specific security practices (govulncheck, Go modules)
- Improve clarity and consistency of security considerations
2025-07-11 14:18:41 +07:00
Tola Leng de79eb963d feat: Allow multiple selections for regional agents.
- Allow multiple regional agents to be selected in the service dialog form, similar to the notification channels.
2025-07-10 23:06:10 +07:00
Tola Leng 7fd6d07c10 Added to detail Development Setup.
- To include Go service operation and Distributed regional agent
2025-07-10 22:41:04 +07:00
Tola Leng 2d2bd790b0 Disable the debug console logs for production 2025-07-10 22:14:17 +07:00
Tola Leng 917d8a6d29 feat: Implement notification channel based on status (Service Dialog Form and Refactor Split ServiceForm into smaller components).
- The Service Dialog Form's Notification Channel field now respects the `notification_status`. If `notification_status` is enabled, the user can select one or multiple notification channel IDs.
2025-07-10 21:07:31 +07:00
Tola Leng ddeb22dc4d Added uptime monitoring detail data and server monitoring schema 2025-07-09 21:59:22 +07:00
Tola Leng e355e771d1 📖 Updated project image URL Readme 2025-07-08 22:01:04 +07:00
Tola Leng f4aeaebe0d Fix: Uptime history bar color for paused/unknown
- Update UptimeBar component to display grey color for "Paused" status and when no data is available.
2025-07-08 20:10:34 +07:00
Tola Leng 6b49119797 Fix: Add regional_status to Service type
- Improved the regional monitoring agent assign to service type
2025-07-08 17:11:35 +07:00
Tola Leng be7c80d7c4 Improved the Uptime Response Time History 2025-07-07 20:33:52 +07:00
Tola Leng 08f31835e1 feat: Improve server overview cards
- Added card background color and grid layout to server overview cards in the server detail page.
2025-07-07 14:49:41 +07:00
Tola Leng 5d2b8b2ed2 Refactor: Split ServerHistory and Charts into smaller components 2025-07-06 15:12:03 +07:00
samang-dauth f0eec90e75 feat: Add system info cards to server detail header
Adds small cards in the server detail page header to display server system information.
2025-07-05 17:00:21 +07:00
Tola Leng 671f22c1d3 Add FUNDING.yml (#63)
* Merge pull request #27 from operacle/develop

feat: Implement uptime monitoring data retention

* Create FUNDING.yml

* add codeowners and pull request workflow
2025-07-03 21:05:56 +07:00
Tola Leng f80b2c8a51 Improved docker compose configuration 2025-07-03 19:50:12 +07:00
Tola Leng dc9e8b981a removed file while the root directory doesn’t need a package.json 2025-07-03 19:33:43 +07:00
samang-dauth 306d2fc336 Fix: Instance monitoring dashboard styling 2025-07-03 17:22:30 +07:00
samang-dauth d072ba7d23 Fix: Server detail page background color
Ensure consistent background color on server detail and docker pages.
2025-07-03 17:17:34 +07:00
Tola Leng 1113ede9a6 Updated and improved the container entrypoint script pipeline 2025-07-02 20:03:29 +07:00
Tola Leng 1e84db4ecc Add new entrypoint for container startup script with hidden pocketbase admin (UI) 2025-07-02 20:03:06 +07:00
samang-dauth 20d5e86d3c Changed logo to use an image instead of the letter. 2025-07-02 19:49:00 +07:00
samang-dauth 549dcc21e4 Change SSL & Domain menu sidebar icon 2025-07-02 19:28:09 +07:00
samang-dauth f97bc4f62f Removed server hostname under container name 2025-07-02 19:10:34 +07:00
samang-dauth d6793f104c Fix: Historical Performance date filter
Ensure date filter respects server ID and displays correct data for 1 day  date filter
2025-07-02 17:44:11 +07:00
Tola Leng bdcf5c75b9 Added Distributed Regional Monitoring Dockerfile 2025-07-01 20:05:52 +07:00
Tola Leng 4d68c6b8a9 Updated Project Features and Roadmap 2025-07-01 19:58:26 +07:00
Tola Leng fda40de155 Update Schedule and Maintenace Overview Card Liner-Gradient 2025-07-01 19:57:51 +07:00
Tola Leng 9b1ccc7f8d Updated SSL Certificate Status Cards linear-gradient 2025-07-01 19:15:24 +07:00
Tola Leng d183ca9791 Updated Uptime Monitoring liner card gradient. 2025-07-01 18:59:55 +07:00
Tola Leng 48ddc2b4c6 Remove system_info card from overview and update status badge 2025-07-01 18:47:36 +07:00
Tola Leng 30bba14e37 Fix: Server history chart and date filter
Ensure the historical performance chart correctly displays server metric data and that the date filter dropdown functions as expected.
2025-07-01 16:23:25 +07:00
Tola Leng 460fcbc7dc Refactor: expand the menu sidebar and add the server and docker detail page 2025-06-30 23:06:38 +07:00
Tola Leng 0b029dc914 feat: Implement docker monitoring dashboard 2025-06-30 23:05:45 +07:00
Tola Leng 691cd4f298 feat: Implement instance monitoring dashboard 2025-06-30 23:01:33 +07:00
Tola Leng 4bfeb42142 Added Download Package URL for fully support both amd64 and arm64 2025-06-30 20:07:49 +07:00
Tola Leng c1fa02c380 Updated Service Operation Executed Display Logs 2025-06-30 19:35:29 +07:00
Tola Leng 15f0e93524 Updated Distributed Regional Monitoring and Roadmap 2025-06-29 15:14:29 +07:00
Tola Leng b37a84734f Change response time chart color
Update the response time chart to use yellow color for Regional Monitoring Data
2025-06-29 00:52:06 +07:00
Tola Leng c979dd24d8 Enhance one-click install script and Regional Agent Dialog
The script was failing to download the package because of an incorrect URL.
2025-06-26 16:07:36 +07:00
Tola Leng fd08a64a05 Fix: Resolve 404 error on public status page 2025-06-26 15:43:41 +07:00
Tola Leng c30bcf9e73 Move Report Menu
Move the Reports menu item to the bottom of the Regional Monitoring section in the main menu.
2025-06-26 14:34:54 +07:00
Tola Leng 7b18a224c1 Update the service detail page to display all monitoring data (default and regional) in separate lines on the graph when "All Monitoring" is selected. Preserve the original functionality of other monitoring source options. 2025-06-26 14:32:23 +07:00
Tola Leng 202a09993c feat: Implement "All Monitoring" option
Update the service detail page to display all monitoring data (default and regional) in separate lines on the graph when "All Monitoring" is selected. Preserve the original functionality of other monitoring source options.
2025-06-26 14:17:09 +07:00
Tola Leng 094f5fcd5e feat: Add "None" option to notification channel.
Adds a "None" option to the notification channel dropdown in the Add SSL Certificate form. This allows users to opt-out of notifications.
2025-06-26 14:10:30 +07:00
Tola Leng ab4f0aa6ea Refactor: Separate data display by monitoring source
and Fix: Display data collection in detail page
2025-06-24 22:11:29 +07:00
Tola Leng 35b11f5a16 Fix: Correct package download URL 2025-06-21 23:26:24 +07:00
Tola Leng 1b11deb40f Fixed bash script package version 2025-06-21 22:14:08 +07:00
Tola Leng 879e106051 Fixed bash script for correct download package URL 2025-06-21 22:09:28 +07:00
Tola Leng 044f9e07fd fixed install-regional-agent bash script 2025-06-21 22:04:49 +07:00
Tola Leng ded509bd10 Enhance one-click install script and Regional Agent Dialog 2025-06-21 21:46:42 +07:00
Tola Leng 1e7c20593f Enhance Add Regional Agent dialog.
Implement one-click installation with automatic script generation and environment variable configuration.
2025-06-21 21:02:40 +07:00
Tola Leng d385f24c28 Restrict removal of default agent
Prevent removal of the default regional agent (agent_id: 1, region_name: Default) in the Regional Monitoring dashboard. This ensures the core service remains active.
2025-06-21 19:52:46 +07:00
Tola Leng 57a65106cb Refactor: Integrate regional agents in service-operation
Integrate distributed monitoring agents for HTTP, PING, TCP, and DNS checks
2025-06-21 17:48:55 +07:00
Tola Leng ea95490f13 Implement Distributed Regional Monitoring Agent with add form, API details, one-click install, and bash script generation. Refactor: Rename API Documentation to Regional Monitoring 2025-06-20 22:46:12 +07:00
406 changed files with 44749 additions and 30503 deletions
+2
View File
@@ -0,0 +1,2 @@
# These owners will be the default owners for everything in
* @tolaleng
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: #[tolaleng]
patreon: # Replace with a single Patreon username
open_collective: #checkcle
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+94
View File
@@ -0,0 +1,94 @@
name: 🐛 Bug Report
description: Report a reproducible bug to help us improve.
title: "[Bug]: "
labels: [bug]
assignees: []
body:
- type: markdown
attributes:
value: |
**Thank you for taking the time to report a bug!**
Please complete the form below to help us reproduce and fix the issue more effectively.
- type: input
id: environment
attributes:
label: Environment
description: |
Describe the environment where the issue occurred.
(e.g., OS, browser, Node.js version, Docker version, etc.)
placeholder: "Example: Ubuntu 22.04, Chrome 125, Node.js 20.3.0"
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: |
A clear and concise description of the issue you encountered.
placeholder: "When I do X, Y happens instead of Z..."
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: |
Provide step-by-step instructions to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: "The app should..."
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or screen recordings to help illustrate the issue.
placeholder: "Drag and drop or paste screenshots here."
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logs
description: |
If applicable, paste any relevant logs or error messages.
Please remove sensitive information before submitting.
render: shell
placeholder: |
```
Error: Something went wrong
at index.js:123:45
```
validations:
required: false
- type: checkboxes
id: terms
attributes:
label: Checklist
description: |
Please confirm the following before submitting:
options:
- label: I have searched existing issues to avoid creating duplicates.
required: true
- label: I have provided sufficient information to reproduce the issue.
required: true
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+49
View File
@@ -0,0 +1,49 @@
name: 📝 Documentation Improvement
description: Suggest improvements or report issues in documentation.
title: "[Docs]: "
labels: [documentation]
assignees: []
body:
- type: markdown
attributes:
value: |
Thank you for helping improve our documentation!
- type: textarea
id: description
attributes:
label: What part of the documentation needs improvement?
description: |
Describe the issue clearly.
placeholder: "The installation guide is missing steps for..."
validations:
required: true
- type: textarea
id: location
attributes:
label: Location
description: |
Provide a link or path to the affected documentation.
placeholder: "https://github.com/operacle/checkcle/docs/INSTALL.md"
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Suggested Change
description: |
How would you improve it?
placeholder: "I suggest adding a section about..."
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Checklist
options:
- label: I have searched existing issues for similar documentation problems.
required: true
@@ -0,0 +1,60 @@
name: 🚀 Feature Request
description: Suggest an idea to improve this project.
title: "[Feature]: "
labels: [enhancement]
assignees: []
body:
- type: markdown
attributes:
value: |
**Thank you for suggesting a feature!**
Please fill out this form so we can better understand your idea.
- type: textarea
id: description
attributes:
label: Feature Description
description: A clear and concise description of the feature youd like to see.
placeholder: "I would like to have..."
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
description: Please explain why this feature would be useful.
placeholder: "This feature would help because..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
placeholder: "I have tried..."
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: "Links to related issues or references..."
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Before submitting, please confirm:
options:
- label: I have searched existing issues to make sure this feature hasnt been requested yet.
required: true
- label: I have described the feature clearly and provided supporting details.
required: true
+54
View File
@@ -0,0 +1,54 @@
name: ❓ Ask for Help
description: Ask a question or request guidance related to this project.
title: "[Question]: "
labels: [question]
assignees: []
body:
- type: markdown
attributes:
value: |
**Thanks for reaching out!**
Please provide as much detail as possible so we can better assist you.
- type: textarea
id: question
attributes:
label: What do you need help with?
description: Clearly describe your question or what you're trying to achieve.
placeholder: |
I am trying to do X, but I'm not sure how to...
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
description: |
Provide any relevant context, related issues, pull requests, or documentation links that could help us understand your question better.
placeholder: "Mention related issues, links to docs, or previous discussions..."
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment Details
description: |
If relevant, provide details about your environment (e.g., OS, browser, Node.js version).
placeholder: "Example: Windows 11, Node.js v20.3.0, Firefox 125"
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Before submitting, please confirm the following:
options:
- label: I have searched existing issues and discussions for a solution.
required: true
- label: I have read the relevant documentation.
required: true
+51
View File
@@ -0,0 +1,51 @@
name: 🛡️ Security Issue
description: Report a potential security vulnerability.
title: "[Security]: "
labels: [security]
assignees: []
body:
- type: markdown
attributes:
value: |
⚠️ **IMPORTANT: Please do NOT disclose sensitive security details in this public issue.**
If you believe you have found a security vulnerability, **please report it privately** to help keep users safe.
📧 **Disclosure Process:**
- Email: [security@checkcle.io](mailto:security@checkcle.io)
- Or follow our [SECURITY.md](../../blob/main/SECURITY.md) policy.
You can use this issue only to let maintainers know that you have sent or will send a report.
- type: textarea
id: summary
attributes:
label: Summary
description: |
Briefly describe the type of vulnerability you believe you have found (without including sensitive details).
placeholder: "Example: Possible SQL injection in the user login endpoint."
validations:
required: true
- type: textarea
id: contact
attributes:
label: Contact Information
description: |
Provide an email or other way for maintainers to contact you if we need clarification.
placeholder: "your.email@example.com"
validations:
required: true
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
description: |
Please confirm:
options:
- label: I will not share sensitive exploit details in this issue.
required: true
- label: I have sent or will send a full report to the private disclosure contact.
required: true
@@ -0,0 +1,38 @@
name: 🌎 Translation Request
description: Request a translation or report translation issues.
title: "[Translation]: "
labels: [translation]
assignees: []
body:
- type: markdown
attributes:
value: |
Help us make this project accessible in more languages!
- type: input
id: language
attributes:
label: Language
description: What language is this about?
placeholder: "Example: Spanish"
validations:
required: true
- type: textarea
id: description
attributes:
label: Details
description: |
Describe what youd like to have translated or what issue you found.
placeholder: "Please translate the Getting Started guide..."
validations:
required: true
- type: checkboxes
id: confirmation
attributes:
label: Checklist
options:
- label: I have searched existing issues for similar requests.
required: true
+3
View File
@@ -22,5 +22,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?
pocketbase.exe
application/src/lib/pocketbase.ts
server/pb_data/
Dockerfile
application/src/lib/pocketbase-prod.ts
+81
View File
@@ -0,0 +1,81 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders of the **CheckCle** project pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all project spaces, and also applies when an individual is officially representing the project in public spaces. Examples include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at:
📧 **Email:** [hello@checkcle.io](mailto:hello@checkcle.io)
All complaints will be reviewed and investigated promptly and fairly.
All project maintainers are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
**1. Correction**
- *Community Impact:* Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
- *Consequence:* A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
**2. Warning**
- *Community Impact:* A violation through a single incident or series of actions.
- *Consequence:* A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
**3. Temporary Ban**
- *Community Impact:* A serious violation of community standards, including sustained inappropriate behavior.
- *Consequence:* A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
**4. Permanent Ban**
- *Community Impact:* Demonstrating a pattern of violation of community standards, including sustained harassment, or showing no remorse after being warned.
- *Consequence:* A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at:
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
For answers to common questions about this code of conduct, see:
https://www.contributor-covenant.org/faq
[homepage]: https://www.contributor-covenant.org
+64 -2
View File
@@ -1,3 +1,36 @@
## 🌐 Select Language
<table align="center">
<tr>
<td align="center">
<a href="CONTRIBUTING.md">
<img src="https://flagcdn.com/24x18/gb.png" alt="English" />
<br/><strong>English</strong>
</a>
</td>
<td align="center">
<a href="docs/CONTRIBUTING_km.md">
<img src="https://flagcdn.com/24x18/kh.png" alt="Khmer" />
<br/><strong>ខ្មែរ</strong>
</a>
</td>
<td align="center">
<a href="docs/CONTRIBUTING_ja.md">
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
<br/><strong>Japanese</strong>
</a>
</td>
</tr>
</table>
<p align="center">
Thank you to all our contributors, users, and supporters for making this project thrive.
</p>
<p align="center">
🚀 <strong>Stay tuned for more updates, features, and improvements.</strong>
</p>
# 🛠️ Contributing to CheckCle
@@ -61,16 +94,45 @@ npm install && npm run dev
#Server Backend
cd server
./pocketbase serve --dir pb_data
./pocketbase serve --dir pb_data
If you're not using localhost, please run with this (./pocketbase serve --http=0.0.0.0:8090 --dir pb_data)
```
### 4. Start Service Check Operation
```bash
#Server Backend
Start Service Operation (Check Service for PING, HTTP, TCP, DNS)
cd server/service-operation
go run main.go (you do not need to change the .env while it's the localhost connection)
```
### 5. Start Distributed Regional Agent
```bash
#### 1. Fork the Repository
Click "Fork" on [GitHub](https://github.com/operacle/Distributed-Regional-Monitoring) to create your own copy.
#### 2. Clone Your Fork
git clone --branch main https://github.com/operacle/Distributed-Regional-Monitoring.git
cd Distributed-Regional-Monitoring
#### 3. Install Go Service (make sure you have installed the Go Service)
Copy .env.example -> .env
Change regional agent configuration in .env file
and Run: go run main.go
```
---
## ✅ Pull Request Process
1. Ensure your code follows the existing style and naming conventions.
2. Write clear, concise commit messages.
3. Push your branch and open a Pull Request (PR) on the `main` branch.
3. Push your branch and open a Pull Request (PR) on the `develop` branch.
4. Provide a meaningful PR description (what/why/how).
5. Link related issues if applicable (e.g. `Closes #12`).
6. Make sure all checks pass (e.g., linting, tests).
+103 -57
View File
@@ -1,4 +1,43 @@
![CheckCle Platform](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png)
## 🌐 Select Language
<table align="center">
<tr>
<td align="center">
<a href="README.md">
<img src="https://flagcdn.com/24x18/gb.png" alt="English" />
<br/><strong>English</strong>
</a>
</td>
<td align="center">
<a href="docs/README_km.md">
<img src="https://flagcdn.com/24x18/kh.png" alt="Khmer" />
<br/><strong>ខ្មែរ</strong>
</a>
</td>
<td align="center">
<a href="docs/README_ja.md">
<img src="https://flagcdn.com/24x18/jp.png" alt="Japanese" />
<br/><strong>Japanese</strong>
</a>
</td>
<td align="center">
<a href="docs/README_zhcn.md">
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
<br/><strong>Chinese</strong>
</a>
</td>
</tr>
</table>
<p align="center">
Thank you to all our contributors, users, and supporters for making this project thrive.
</p>
<p align="center">
🚀 <strong>Stay tuned for more updates, features, and improvements.</strong>
</p>
![CheckCle Platform](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/server-detail-page.png)
# 🚀 What is CheckCle?
@@ -6,13 +45,16 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
## 🎯 Live Demo
👉 **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io)
User: admin@example.com | Passwd: Admin123456
## 🌟 Core Features
### 📝 Roadmap : [DEVELOPMENT_ROADMAP](docs/DEVELOPMENT_ROADMAP.md)
### Uptime Services & Infrastructure Server Monitoring
### Uptime Services & Infrastructure Server Monitoring
- Monitor HTTP, DNS, and Ping protocols
- Monitor TCP-based, API services (e.g., FTP, SMTP, HTTP)
- Track detail uptime, response times, and performance issues
- Distributed Regional Monitoring
- Incident History (UP/DOWN/WARNING/PAUSE)
- SSL & Domain Monitoring (Domain, Issuer, Expiration Date, Days Left, Status, Last Notified)
- Infrastructure Server Monitoring, Supports Linux (🐧 Debian, Ubuntu, CentOS, Red Hat, etc.) and Windows (Beta). And Servers metrics like CPU, RAM, disk usage, and network activity) with an one-line installation angent script.
@@ -28,26 +70,9 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
* ✅ x86_64 PCs, laptops, servers (amd64)
* ✅ Modern Raspberry Pi 3/4/5 with (64-bit OS), Apple Silicon Macs (arm64)
### Install CheckCle using one of the options below.
### Install CheckCle using one of the following methods:
1. CheckCle One-Click Installation - Just copy and run on terminal
```bash
curl -fsSL https://checkcle.io/install.sh | bash
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Install with Docker compose Configuration.
1. Install with Docker Compose Configuration (Recommended)
```bash
version: '3.9'
@@ -67,6 +92,18 @@ services:
hard: 8192
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Admin Web Management
Default URL: http://0.0.0.0:8090
@@ -76,31 +113,12 @@ services:
4. Follow the Quick Start Guide at https://docs.checkcle.io
###
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png)
![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-detailpage.png)
![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/maintenance-dahboard.png)
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-1.4.png)
![Service Detail Page](https://cdn.checkcle.io/images/uptime/uptime-regional-detail.png)
![checkcle-server-instance](https://cdn.checkcle.io/images/server/server-list.png)
![SSL Monitoring](https://cdn.checkcle.io/images/ssl-domain/ssl-list.png)
![Notification System](https://cdn.checkcle.io/general/powerfull_notification.png)
## 📝 Development Roadmap
- ✅ Health check & uptime monitoring (HTTP)
- ✅ Dashboard UI with live stats
- ✅ Auth with Multi-users system (admin)
- ✅ Notifications (Telegram)
- ✅ Docker containerization
- ✅ CheckCle Website
- ✅ CheckCle Demo Server
- ✅ SSL & Domain Monitoring
- ✅ Schedule Maintenance
- ✅ Incident Management
- [ ] Uptime monitoring (PING - Inprogress)
- [ ] Infrastructure Server Monitoring
- ✅ Operational Status / Public Status Pages
- [ ] Uptime monitoring (TCP, PING, DNS)
- ✅ System Setting Panel and Mail Settings
- ✅ User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
- ✅ Data Retention & Automate Strink (Muti Options to Shrink Data & Database )
- ✅ Open-source release with full documentation
## 🌟 CheckCle for Communities?
- **Built with Passion**: Created by an open-source enthusiast for the community
@@ -109,6 +127,42 @@ services:
---
## Sponsors
Sponsorships will no longer be accepted. From now on, support will only be accepted in the form of ecosystem and community partnerships that provide infrastructure such as cloud servers, domains, or hosting credits.
If youre a tech company interested in supporting CheckCle, please contact the author directly at tolaleng@checkcle.io
### 🤝 Ecosystem & Community Partner
<div style="display: flex; align-items: center; gap: 10px;">
<a href="https://github.com/gitbookio">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75"
style="border-radius: 50%;"
alt="GitBook Logo" />
</a>
<a href="https://www.cloudflare.com">
<img src="https://cdn.checkcle.io/images/sponsor/cloudflare-checkcle_logo.png"
height="60"
alt="Cloudflare Logo" />
</a>
<a href="https://m.do.co/c/0c27ef82475f">
<img src="https://cdn.checkcle.io/images/sponsor/digitalocean_checkcle.png"
height="50"
alt="DigitalOcean Logo" />
</a>
</div>
---
## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
---
## 🤝 Ways to Contribute
Here are some ways you can help improve CheckCle:
@@ -117,26 +171,18 @@ Here are some ways you can help improve CheckCle:
- 🌟 **Suggest Features** Have an idea? Start a [Discussion](https://github.com/operacle/checkcle/discussions) or open a Feature Request issue.
- 🛠 **Submit Pull Requests** Improve the code, fix bugs, add features, or enhance the docs.
- 📝 **Improve Documentation** Even a typo fix helps!
- 🌍 **Spread the Word** Star ⭐ the repo, share it on socials, and invite others to contribute!
- 🌍 **Spread the Word** Star ⭐ the [CheckCle](https://github.com/operacle/checkcle.git) repo, share it on socials, and invite others to contribute!
---
## 🌍 Stay Connected
- Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- Community Channels: Engage via discussions and issues!
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
## 📜 License
CheckCle is released under the MIT License.
---
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=operacle/checkcle&type=Date)](https://www.star-history.com/#operacle/checkcle&Date)
Stay informed, stay online, with CheckCle! 🌐
+9 -10
View File
@@ -2,7 +2,7 @@
## 📦 Project: [checkcle](https://github.com/operacle/checkcle)
**checkcle** is a lightweight, TypeScript-based built for uptime and server monitoring tools.
**checkcle** is a self-hosted uptime and server monitoring tool built with TypeScript and Go.
We care about the security and privacy of users running this project in production environments.
@@ -47,22 +47,21 @@ We support the latest stable release of `checkcle`. Security patches may also be
## 🔍 Security Practices
This project adheres to the following practices to enhance security:
CheckCle follows these practices to improve overall security:
- 🔎 Regular vulnerability checks using `npm audit`
- ⛓️ Dependency pinning via `package-lock.json`
- ✅ Type-safe code using `TypeScript`
- 🧪 Continuous testing and CI
- 🔎 Regular vulnerability scanning (npm audit for JavaScript dependencies, govulncheck for Go modules)
- ⛓️ Dependency pinning (package-lock.json and Go modules)
- ✅ Type-safe code in TypeScript and memory-safe design in Go
- 🧪 Continuous testing and CI pipelines
- 🔐 No data is stored or transmitted unless explicitly configured by the user
- 🧑‍💻 All contributions are reviewed before being merged
- 🧑‍💻 All code contributions are reviewed before merging
---
## ⚠️ Known Security Limitations
- `checkcle` makes outbound HTTPS requests to check certificate details — avoid running in untrusted or high-risk environments without proper network policies.
- Output may contain certificate metadata (e.g., CN, SANs, expiry dates) — avoid exposing this to public logs unless sanitized.
- The data may be lost upon system restarts or crashes. Always ensure that backup and recovery mechanisms are in place in production environments.
- Outbound HTTPS requests: CheckCle agents perform outbound HTTPS connections to send metric data to the backend server. Avoid deploying in untrusted or high-risk environments without appropriate network policies and monitoring.
- The data may be lost upon system restarts or crashes. Always ensure that backup (pb_data) and recovery mechanisms are in place in production environments.
---
+24 -21
View File
@@ -1,26 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle is an open-source monitoring stack</title>
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle - Dev</title>
<meta name="description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@tlengoss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+469 -15
View File
@@ -8,6 +8,7 @@
"name": "vite_react_shadcn_ts",
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -27,7 +28,7 @@
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
@@ -43,6 +44,8 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"pocketbase": "^0.19.0",
@@ -91,13 +94,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
@@ -645,6 +645,21 @@
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -664,6 +679,26 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
@@ -1811,12 +1846,12 @@
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
"integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1833,6 +1868,62 @@
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
@@ -2225,6 +2316,103 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@react-aria/focus": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.5",
"@react-aria/utils": "^3.30.1",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.25.5",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.30.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.10.8",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -2674,6 +2862,15 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@swc/types": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
@@ -2738,6 +2935,33 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -2832,6 +3056,13 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
@@ -2853,6 +3084,13 @@
"@types/react": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@@ -3206,6 +3444,18 @@
"node": ">=10"
}
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -3250,6 +3500,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3318,6 +3578,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3358,6 +3630,26 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3843,6 +4135,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3856,6 +4160,16 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4064,6 +4378,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4424,6 +4748,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4647,6 +4977,20 @@
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4848,6 +5192,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/jspdf": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.7",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5208,6 +5579,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5450,6 +5828,16 @@
],
"license": "MIT"
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -5708,10 +6096,11 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/resolve": {
"version": "1.22.8",
@@ -5750,6 +6139,16 @@
"node": ">=0.10.0"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
@@ -5883,6 +6282,16 @@
"node": ">=0.10.0"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -6039,6 +6448,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@@ -6095,6 +6520,16 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6308,12 +6743,31 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
+2
View File
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -45,6 +46,7 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"framer-motion": "12.23.12",
"input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+151 -49
View File
@@ -1,64 +1,166 @@
import React, { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/sonner";
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@/components/ui/sonner';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { LanguageProvider } from '@/contexts/LanguageContext';
import { SidebarProvider } from '@/contexts/SidebarContext';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { ThemeProvider } from "./contexts/ThemeContext";
import { LanguageProvider } from "./contexts/LanguageContext";
import { SidebarProvider } from "./contexts/SidebarContext";
// Pages
import Index from '@/pages/Index';
import Login from '@/pages/Login';
import Dashboard from '@/pages/Dashboard';
import ServiceDetail from '@/pages/ServiceDetail';
import Profile from '@/pages/Profile';
import Settings from '@/pages/Settings';
import OperationalPage from '@/pages/OperationalPage';
import ScheduleIncident from '@/pages/ScheduleIncident';
import SslDomain from '@/pages/SslDomain';
import PublicStatusPage from '@/pages/PublicStatusPage';
import NotFound from '@/pages/NotFound';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
import Index from "./pages/Index";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import InstanceMonitoring from "./pages/InstanceMonitoring";
import ContainerMonitoring from "./pages/ContainerMonitoring";
import ServiceDetail from "./pages/ServiceDetail";
import SslDomain from "./pages/SslDomain";
import ScheduleIncident from "./pages/ScheduleIncident";
import OperationalPage from "./pages/OperationalPage";
import RegionalMonitoring from "./pages/RegionalMonitoring";
import Settings from "./pages/Settings";
import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
import PublicStatusPage from "./pages/PublicStatusPage";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import ServerDetail from "./pages/ServerDetail";
function App() {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ThemeProvider>
<LanguageProvider>
<SidebarProvider>
<ErrorBoundary>
<Router>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/service/:id" element={<ServiceDetail />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/operational-page" element={<OperationalPage />} />
<Route path="/schedule-incident" element={<ScheduleIncident />} />
<Route path="/ssl-domain" element={<SslDomain />} />
<Route path="/status/:slug" element={<PublicStatusPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
<QueryClientProvider client={queryClient}>
<Toaster />
</ErrorBoundary>
<Routes>
{/* Public routes */}
<Route path="/public/:slug" element={<PublicStatusPage />} />
{/* Protected routes */}
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<Index />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/instance-monitoring"
element={
<ProtectedRoute>
<InstanceMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/server-detail/:serverId"
element={
<ProtectedRoute>
<ServerDetail />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring/:serverId"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route
path="/regional-monitoring"
element={
<ProtectedRoute>
<RegionalMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</QueryClientProvider>
</SidebarProvider>
</LanguageProvider>
</ThemeProvider>
</QueryClientProvider>
</BrowserRouter>
);
}
export default App;
export default App;
+5 -5
View File
@@ -11,19 +11,19 @@ const api = {
* Handle API requests
*/
async handleRequest(path, method, body) {
console.log(`API request: ${method} ${path}`, body);
// console.log(`API request: ${method} ${path}`, body);
// Route to the appropriate handler
if (path === '/api/realtime') {
console.log("Routing to realtime handler");
// console.log("Routing to realtime handler");
return await realtime(body);
} else if (path === '/api/settings' || path.startsWith('/api/settings/')) {
console.log("Routing to settings handler");
// console.log("Routing to settings handler");
return await settingsApi(body, path);
}
// Return 404 for unknown routes
console.error(`Endpoint not found: ${path}`);
// console.error(`Endpoint not found: ${path}`);
return {
status: 404,
json: {
@@ -40,7 +40,7 @@ const originalFetch = window.fetch;
window.fetch = async (url, options = {}) => {
// Check if this is an API request to our mock endpoints
if (typeof url === 'string' && url.startsWith('/api/')) {
console.log('Intercepting API request:', url, options);
// console.log('Intercepting API request:', url, options);
try {
let body = {};
+1 -1
View File
@@ -2,7 +2,7 @@
// This file handles realtime notifications in a client-side environment
// In a production app, this would be a server-side endpoint
console.log("API Realtime endpoint loaded");
//console.log("API Realtime endpoint loaded");
// Simple implementation that simulates sending notifications
export default async function handler(req) {
@@ -3,7 +3,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
import { SettingsApiResponse } from '../types';
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
let subject = 'Test Email from ReamStack';
let subject = 'Test Email from CheckCle';
let htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -13,7 +13,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p>If you received this email, your SMTP configuration is working correctly.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">
Sent from ReamStack Monitoring System<br>
Sent from CheckCle Monitoring System<br>
Template: ${template}<br>
${data.collection ? `Collection: ${data.collection}` : ''}
</p>
@@ -24,7 +24,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
switch (template) {
case 'verification':
subject = 'Email Verification Test - ReamStack';
subject = 'Email Verification Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -37,14 +37,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'password-reset':
subject = 'Password Reset Test - ReamStack';
subject = 'Password Reset Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -57,14 +57,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'email-change':
subject = 'Email Change Confirmation Test - ReamStack';
subject = 'Email Change Confirmation Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -76,7 +76,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Template:</strong> Email Change Confirmation</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
@@ -178,10 +178,6 @@ export const sendTestEmail = async (data: any): Promise<SettingsApiResponse> =>
smtpPort: smtpSettings.port || 587
});
// For now, we'll simulate a successful email send
// In a real implementation, you would integrate with your email service here
// This could be nodemailer, SendGrid, or your PocketBase email system
// Simulate processing time
console.log('Simulating email send...');
await new Promise(resolve => setTimeout(resolve, 1000));
@@ -2,7 +2,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
import { SettingsApiResponse } from '../types';
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
let subject = 'Test Email from ReamStack';
let subject = 'Test Email from CheckCle';
let htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -12,7 +12,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p>If you received this email, your SMTP configuration is working correctly.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">
Sent from ReamStack Monitoring System<br>
Sent from CheckCle Monitoring System<br>
Template: ${template}<br>
${data.collection ? `Collection: ${data.collection}` : ''}
</p>
@@ -23,7 +23,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
switch (template) {
case 'verification':
subject = 'Email Verification Test - ReamStack';
subject = 'Email Verification Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -36,14 +36,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'password-reset':
subject = 'Password Reset Test - ReamStack';
subject = 'Password Reset Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -56,14 +56,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'email-change':
subject = 'Email Change Confirmation Test - ReamStack';
subject = 'Email Change Confirmation Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -75,7 +75,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Template:</strong> Email Change Confirmation</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
+2 -2
View File
@@ -8,7 +8,7 @@ import { testEmail } from './actions/testEmail';
* Settings API handler
*/
const settingsApi = async (body: any, path?: string) => {
console.log('Settings API called with path:', path, 'body:', body);
// console.log('Settings API called with path:', path, 'body:', body);
// Handle test email endpoint specifically
if (path === '/api/settings/test/email') {
@@ -18,7 +18,7 @@ const settingsApi = async (body: any, path?: string) => {
// Handle regular settings API with action-based routing
const action = body?.action;
console.log('Settings API called with action:', action, 'data:', body?.data);
// console.log('Settings API called with action:', action, 'data:', body?.data);
switch (action) {
case 'getSettings':
@@ -0,0 +1,16 @@
import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
import { authService } from '@/services/authService';
interface ProtectedRouteProps {
children: ReactNode;
}
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
if (!authService.isAuthenticated()) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
@@ -1,4 +1,3 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@@ -7,6 +6,7 @@ import { StatusCards } from "./StatusCards";
import { ServiceFilters } from "./ServiceFilters";
import { ServicesTable } from "./ServicesTable";
import { AddServiceDialog } from "@/components/services/AddServiceDialog";
import { useLanguage } from "@/contexts/LanguageContext";
interface DashboardContentProps {
services: Service[];
@@ -14,16 +14,24 @@ interface DashboardContentProps {
error: Error | null;
}
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
export const DashboardContent = ({
services,
isLoading,
error,
}: DashboardContentProps) => {
const { t } = useLanguage();
const [filter, setFilter] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>("");
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
// Filter services based on search term and type filter
const filteredServices = services.filter(service => {
const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
const filteredServices = services.filter((service) => {
const matchesSearch =
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url &&
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter =
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
return matchesSearch && matchesFilter;
});
@@ -31,40 +39,42 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading service data.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
<Button onClick={() => window.location.reload()}>{t("retry")}</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl ">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">Overview</h2>
<Button
<h2 className="text-2xl font-bold text-foreground">
{t("overview")}
</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> New Service
<Plus className="w-4 h-4 mr-2" /> {t("newService")}
</Button>
</div>
<StatusCards services={services} />
<ServiceFilters
<ServiceFilters
filter={filter}
setFilter={setFilter}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
servicesCount={filteredServices.length}
/>
<div className="flex-1 flex flex-col pb-6">
<ServicesTable services={filteredServices} />
</div>
</div>
<AddServiceDialog
<AddServiceDialog
open={isAddDialogOpen}
onOpenChange={setIsAddDialogOpen}
/>
+215 -71
View File
@@ -1,15 +1,39 @@
import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut, Grid3x3 } from "lucide-react";
import {
Moon,
PanelLeft,
PanelLeftClose,
Sun,
Globe,
FileText,
Github,
Twitter,
MessageSquare,
Bell,
User,
Settings,
LogOut,
CaseUpper,
BookA,
CaseSensitive,
CaseLower,
ALargeSmall,
} from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import QuickActionsDialog from "./QuickActionsDialog";
import useDynamicFontSize from "@/contexts/FontSizeContext";
interface HeaderProps {
currentUser: AuthUser | null;
@@ -22,14 +46,13 @@ export const Header = ({
currentUser,
onLogout,
sidebarCollapsed,
toggleSidebar
toggleSidebar,
}: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage();
const [greeting, setGreeting] = useState<string>("");
const { systemName } = useSystemSettings();
const navigate = useNavigate();
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
// Set greeting based on time of day
useEffect(() => {
@@ -53,154 +76,275 @@ export const Header = ({
// Log avatar data for debugging
useEffect(() => {
if (currentUser) {
console.log("Avatar URL in Header:", currentUser.avatar);
//console.log("Avatar URL in Header:", currentUser.avatar);
}
}, [currentUser]);
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
let avatarUrl = '';
let avatarUrl = "";
if (currentUser?.avatar) {
// If it's a relative path from the public folder, make sure it's resolved properly
if (currentUser.avatar.startsWith('/upload/profile/')) {
if (currentUser.avatar.startsWith("/upload/profile/")) {
avatarUrl = currentUser.avatar;
} else {
avatarUrl = currentUser.avatar;
}
console.log("Final avatar URL:", avatarUrl);
}
const [fontSize, setFontSize] = useState(() => {
const savedFontSize = localStorage.getItem("fontSize");
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
});
useDynamicFontSize(fontSize);
return (
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
{/* Grid Pattern Overlay - Similar to StatusCards */}
<div className="absolute inset-0 z-0">
<div
className="w-full h-full"
style={{
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
backgroundSize: '20px 20px'
<div
className="w-full h-full dark:bg-black"
style={{
backgroundImage: `linear-gradient(${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px),
linear-gradient(90deg, ${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
}}
>
<div className="w-full h-full backdrop-blur-[1px]"></div>
</div>
</div>
{/* Header Content */}
<div className="flex items-center gap-4 z-10">
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</Button>
{/* Quick Actions Button */}
<Button
variant="ghost"
size="icon"
onClick={() => setQuickActionsOpen(true)}
<div
className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${
!sidebarCollapsed ? "hidden" : ""
}`}
>
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className={`h-6 w-6 `}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="mr-2"
>
<Grid3x3 className="h-5 w-5 text-green-500" />
{sidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</Button>
<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 className="flex items-center space-x-4 z-10">
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={toggleTheme}
>
<span className="sr-only">Toggle theme</span>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{theme === "dark" ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<Globe className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("en")}
className={language === "en" ? "bg-accent" : ""}
>
{t("english")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("km")}
className={language === "km" ? "bg-accent" : ""}
>
{t("khmer")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("de")}
className={language === "de" ? "bg-accent" : ""}
>
{t("Deutsch")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("ko")}
className={language === "ko" ? "bg-accent" : ""}
>
{t("korean")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("ja")}
className={language === "ja" ? "bg-accent" : ""}
>
{t("japanese")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setLanguage("zhcn")}
className={language === "zhcn" ? "bg-accent" : ""}
>
{t("simplifiedChinese")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Documentation */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
>
<span className="sr-only">{t("documentation")}</span>
<FileText className="w-4 h-4" />
</Button>
{/* GitHub */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
onClick={() =>
window.open("https://github.com/operacle/checkcle", "_blank")
}
>
<span className="sr-only">GitHub</span>
<Github className="w-4 h-4" />
</Button>
{/* X (Twitter) */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/tlengoss", "_blank")}
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
</Button>
{/* Discord */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
>
<span className="sr-only">Discord</span>
<MessageSquare className="w-4 h-4" />
</Button>
{/* Notifications */}
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" />
</Button>
{/* Fond Size */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<BookA className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev + 1)}
className={`cursor-pointer`}
>
<span className={`text-[15px] font-semibold`}> Aa +</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev - 1)}
className={`cursor-pointer`}
>
<span className={`text-[12px] font-semibold`}> Aa -</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* User Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-3 p-2">
<Avatar className="h-10 w-10">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar>
<div className="flex flex-col space-y-1">
<span className="font-medium">{currentUser?.name || 'User'}</span>
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
<span className="font-medium">
{currentUser?.name || "User"}
</span>
<span className="text-xs text-muted-foreground truncate">
{currentUser?.email}
</span>
</div>
</div>
<DropdownMenuSeparator />
@@ -213,16 +357,16 @@ export const Header = ({
<span>{t("settings")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500">
<DropdownMenuItem
onClick={onLogout}
className="text-red-500 focus:text-red-500"
>
<LogOut className="mr-2 h-4 w-4" />
<span>{t("logout")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Quick Actions Dialog */}
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
</header>
);
};
};
@@ -1,145 +0,0 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogClose
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface QuickActionsDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
const { t } = useLanguage();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
<DialogClose className="absolute right-4 top-4">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogClose>
<DialogHeader>
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
<DialogDescription>
{t('quickActionsDescription')}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
{/* Website Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorWebsiteDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorMultipleEndpoints')}</li>
<li>{t('trackResponseTimes')}</li>
<li>{t('setCustomAlerts')}</li>
</ul>
</div>
</div>
{/* Server Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorServerDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorCPURam')}</li>
<li>{t('trackNetworkMetrics')}</li>
<li>{t('viewProcesses')}</li>
</ul>
</div>
</div>
{/* SSL Certificate Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('sslCertificateDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('trackSSLExpiration')}</li>
<li>{t('getEarlyRenewal')}</li>
<li>{t('monitorMultipleDomains')}</li>
</ul>
</div>
</div>
{/* Incidents Management Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('incidentsManagementDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createTrackIncidents')}</li>
<li>{t('assignIncidents')}</li>
<li>{t('monitorResolution')}</li>
</ul>
</div>
</div>
{/* Operational Status Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('operationalStatusDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createCustomStatus')}</li>
<li>{t('displayRealTime')}</li>
<li>{t('shareIncidentUpdates')}</li>
</ul>
</div>
</div>
{/* Reports & Analytics Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('reportsAnalyticsDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('generateUptimeReports')}</li>
<li>{t('analyzePerformance')}</li>
<li>{t('exportMonitoringData')}</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button variant="outline" onClick={() => setIsOpen(false)}>
{t('close')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default QuickActionsDialog;
@@ -1,8 +1,14 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceFiltersProps {
filter: string;
@@ -12,28 +18,31 @@ interface ServiceFiltersProps {
servicesCount: number;
}
export const ServiceFilters = ({
filter,
setFilter,
searchTerm,
export const ServiceFilters = ({
filter,
setFilter,
searchTerm,
setSearchTerm,
servicesCount
servicesCount,
}: ServiceFiltersProps) => {
const { t } = useLanguage();
return (
<div className="mb-6 flex justify-between items-center">
<div className="mb-6 flex justify-between items-center ">
<div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
<h3 className="text-xl font-semibold mr-2 text-foreground">
{t("currentlyMonitoring")}
</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount}
</span>
</div>
<div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder="All Types" />
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
<SelectValue placeholder={t("allTypes")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="all">{t("allTypes")}</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem>
@@ -41,9 +50,9 @@ export const ServiceFilters = ({
</SelectContent>
</Select>
<div className="relative">
<Input
className="w-72 bg-card border-border"
placeholder="Search"
<Input
className="w-72 bg-card border-border"
placeholder={t("search")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -1,4 +1,3 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader";
@@ -11,12 +10,25 @@ interface SidebarProps {
export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme();
return (
<div className={`${collapsed ? 'w-16' : 'w-64'} ${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} border-r flex flex-col transition-all duration-300 h-full`}>
<div
className={`
${
theme === "dark"
? "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} />
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} />
<div className="items-center ml-2 p-2 h-30 border-t ">
Version: latest
</div>
</div>
);
};
};
@@ -3,12 +3,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
import { Service } from "@/services/serviceService";
import { useTheme } from "@/contexts/ThemeContext";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface StatusCardsProps {
services: Service[];
}
export const StatusCards = ({ services }: StatusCardsProps) => {
const { t } = useLanguage();
// Count services by status
const upServices = services.filter(s => s.status === "up").length;
const downServices = services.filter(s => s.status === "down").length;
@@ -27,22 +30,22 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
} relative z-10`}
style={{
background: theme === 'dark'
? "linear-gradient(135deg, rgba(67, 160, 71, 0.8) 0%, rgba(102, 187, 106, 0.6) 100%)"
: "linear-gradient(135deg, #43a047 0%, #66bb6a 100%)"
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(102, 187, 106, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #66bb6a 100%)"
}}
>
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 opacity-10">
<div className="w-full h-full"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px),
backgroundImage: `linear-gradient(#000 1px, transparent 1px),
linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{upServices}</span>
@@ -59,22 +62,22 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
} relative z-10`}
style={{
background: theme === 'dark'
? "linear-gradient(135deg, rgba(229, 57, 53, 0.8) 0%, rgba(239, 83, 80, 0.6) 100%)"
: "linear-gradient(135deg, #e53935 0%, #ef5350 100%)"
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(239, 83, 80, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #ef5350 100%)"
}}
>
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 opacity-10">
<div className="w-full h-full"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px),
backgroundImage: `linear-gradient(#000 1px, transparent 1px),
linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{downServices}</span>
@@ -91,22 +94,22 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
} relative z-10`}
style={{
background: theme === 'dark'
? "linear-gradient(135deg, rgba(25, 118, 210, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)"
: "linear-gradient(135deg, #1976d2 0%, #42a5f5 100%)"
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #42a5f5 100%)"
}}
>
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 opacity-10">
<div className="w-full h-full"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px),
backgroundImage: `linear-gradient(#000 1px, transparent 1px),
linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{pausedServices}</span>
@@ -123,22 +126,22 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
} relative z-10`}
style={{
background: theme === 'dark'
? "linear-gradient(135deg, rgba(255, 152, 0, 0.8) 0%, rgba(255, 183, 77, 0.6) 100%)"
: "linear-gradient(135deg, #ff9800 0%, #ffb74d 100%)"
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(255, 183, 77, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #ffb74d 100%)"
}}
>
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 opacity-10">
<div className="w-full h-full"
style={{
backgroundImage: `linear-gradient(#fff 1px, transparent 1px),
backgroundImage: `linear-gradient(#000 1px, transparent 1px),
linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{warningServices}</span>
@@ -1,4 +1,3 @@
import React from "react";
import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData";
@@ -7,9 +6,15 @@ interface MainNavigationProps {
collapsed: boolean;
}
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
export const MainNavigation: React.FC<MainNavigationProps> = ({
collapsed,
}) => {
return (
<nav className="my-2 mx-1 py-1 px-1">
<nav
className={`my-2 mx-1 py-1 px-1 dark:bg-black ${
collapsed ? "hidden" : ""
}`}
>
{mainMenuItems.map((item) => (
<MenuItem
key={item.id}
@@ -24,4 +29,4 @@ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) =>
))}
</nav>
);
};
};
@@ -1,4 +1,3 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
@@ -22,7 +21,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
translationKey,
color,
hasNavigation,
collapsed
collapsed,
}) => {
const { theme } = useTheme();
const { t } = useLanguage();
@@ -32,9 +31,8 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (hasNavigation && path) {
// Use navigate instead of window.location to prevent full page reload
navigate(path, { replace: false });
}
};
@@ -43,12 +41,29 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const mainIconSize = "h-6 w-6";
return (
<div
className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${isActive ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200 cursor-pointer`}
<div
className={`
${collapsed ? "p-3" : "p-2 pl-3"}
mb-1 rounded-lg
${
isActive
? theme === "dark"
? "bg-drak"
: "bg-sidebar-accent "
: `hover:${theme === "dark" ? "bg-gray-800" : "bg-sidebar-accent "}`
}
flex items-center
${collapsed ? "justify-center" : ""}
cursor-pointer
`}
onClick={handleClick}
>
<Icon className={`${mainIconSize} ${color}`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t(translationKey)}</span>}
{!collapsed && (
<span className="ml-2.5 font-medium text-foreground tracking-wide ">
{t(translationKey)}
</span>
)}
</div>
);
};
};
@@ -1,10 +1,13 @@
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { Settings, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { settingsMenuItems } from "./navigationData";
@@ -17,19 +20,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
const { t } = useLanguage();
const location = useLocation();
const navigate = useNavigate();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
"general"
);
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
const savedValue = localStorage.getItem("settingsPanelOpen");
// Check if the saved value exists and is "true"
return savedValue === "true";
});
// Update active settings item based on URL
useEffect(() => {
if (location.pathname === '/settings') {
if (location.pathname === "/settings") {
const params = new URLSearchParams(location.search);
const panel = params.get('panel');
const panel = params.get("panel");
if (panel) {
setActiveSettingsItem(panel);
}
}
}, [location]);
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
}, [location, settingsPanelOpen]);
const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item);
@@ -43,15 +54,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
};
const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
return `p-2 ${
isActive
? theme === "dark"
? "bg-[#1a1a1a]"
: "bg-sidebar-accent"
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
} rounded-lg flex items-center `;
};
if (collapsed) {
const mainIconSize = "h-6 w-6";
return (
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<div
onClick={(e) => handleMenuItemClick('/settings', e)}
<div
className={`border-t ${
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"
>
<Settings className={`${mainIconSize} text-purple-400`} />
@@ -61,33 +84,59 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
}
return (
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
<div
className={`flex-1 flex flex-col border-t ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-sidebars"
} p-4 ${collapsed ? "hidden" : ""}`}
>
<Collapsible
open={settingsPanelOpen}
onOpenChange={setSettingsPanelOpen}
className="w-full flex flex-col flex-1"
>
<CollapsibleTrigger
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
}`}
>
<div className="flex items-center">
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
<span className="font-medium tracking-wide">
{t("settingPanel")}
</span>
</div>
<div className="flex items-center">
<Settings className="h-4 w-4 mr-1" />
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
<ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${
settingsPanelOpen ? "rotate-180" : ""
}`}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#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">
<ScrollArea className="h-full">
<div className="space-y-2 pr-4">
<div className="space-y-2 pr-4 cursor-pointer">
{settingsMenuItems.map((item) => (
<div
<div
key={item.id}
className={getMenuItemClasses(activeSettingsItem === item.id)}
className={getMenuItemClasses(
activeSettingsItem === item.id
)}
onClick={(e) => {
handleMenuItemClick(`/settings?panel=${item.id}`, e);
handleSettingsItemClick(item.id);
}}
>
<item.icon className="h-4 w-4 mr-2" />
<span className="text-sm">{t(item.translationKey)}</span>
<span className="text-base">{t(item.translationKey)}</span>
</div>
))}
</div>
@@ -97,4 +146,4 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
</Collapsible>
</div>
);
};
};
@@ -1,4 +1,3 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
@@ -10,11 +9,17 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme();
return (
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
<div className="h-8 w-8 bg-green-500 rounded flex items-center justify-center mr-2">
<span className="text-white font-bold">C</span>
<div
className={`p-[15.7px] ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-white"
} border-b flex items-center ${collapsed ? "justify-center" : ""}`}
>
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
</div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div>
);
};
};
@@ -1,5 +1,5 @@
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
import { Globe, Boxes, Layers, Calendar, BarChart2, LineChart, MapPin, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
export const mainMenuItems = [
{
@@ -12,16 +12,16 @@ export const mainMenuItems = [
},
{
id: 'instance-monitoring',
path: null,
path: '/instance-monitoring',
icon: Boxes,
translationKey: 'instanceMonitoring',
color: 'text-blue-400',
hasNavigation: false
hasNavigation: true
},
{
id: 'ssl-domain',
path: '/ssl-domain',
icon: Radar,
icon: Layers,
translationKey: 'sslDomain',
color: 'text-cyan-400',
hasNavigation: true
@@ -42,6 +42,14 @@ export const mainMenuItems = [
color: 'text-amber-400',
hasNavigation: true
},
{
id: 'regional-monitoring',
path: '/regional-monitoring',
icon: MapPin,
translationKey: 'regionalMonitoring',
color: 'text-indigo-400',
hasNavigation: true
},
{
id: 'reports',
path: null,
@@ -49,14 +57,6 @@ export const mainMenuItems = [
translationKey: 'reports',
color: 'text-rose-400',
hasNavigation: false
},
{
id: 'api-documentation',
path: null,
icon: FileText,
translationKey: 'apiDocumentation',
color: 'text-indigo-400',
hasNavigation: false
}
];
@@ -0,0 +1,97 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody } from "@/components/ui/table";
import { DockerContainer } from "@/types/docker.types";
import { DockerMetricsDialog } from "./DockerMetricsDialog";
import {
DockerTableSearch,
DockerTableHeader,
DockerTableRow,
DockerEmptyState
} from "./table";
interface DockerContainersTableProps {
containers: DockerContainer[];
isLoading: boolean;
onRefresh: () => void;
}
export const DockerContainersTable = ({ containers, isLoading, onRefresh }: DockerContainersTableProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [selectedContainer, setSelectedContainer] = useState<DockerContainer | null>(null);
const [metricsDialogOpen, setMetricsDialogOpen] = useState(false);
const filteredContainers = containers.filter(container =>
container.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
container.docker_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
container.hostname.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleContainerAction = (action: string, containerId: string, containerName: string) => {
console.log(`${action} action for container ${containerName} (${containerId})`);
// TODO: Implement container actions
};
const handleRowClick = (container: DockerContainer) => {
setSelectedContainer(container);
setMetricsDialogOpen(true);
};
const handleViewMetrics = (container: DockerContainer) => {
setSelectedContainer(container);
setMetricsDialogOpen(true);
};
return (
<>
<Card className="w-full bg-transparent border-0 shadow-none">
<CardHeader className="pb-4 px-0">
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="text-lg sm:text-xl font-semibold">Docker Containers</CardTitle>
<DockerTableSearch
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
onRefresh={onRefresh}
isLoading={isLoading}
/>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<div className="min-w-full inline-block align-middle">
<div className="overflow-hidden border border-border rounded-lg shadow-sm">
<Table>
<DockerTableHeader />
<TableBody>
{filteredContainers.length === 0 ? (
<DockerEmptyState searchTerm={searchTerm} />
) : (
filteredContainers.map((container) => (
<DockerTableRow
key={container.id}
container={container}
onRowClick={handleRowClick}
onContainerAction={handleContainerAction}
onViewMetrics={handleViewMetrics}
/>
))
)}
</TableBody>
</Table>
</div>
</div>
</div>
</CardContent>
</Card>
<DockerMetricsDialog
container={selectedContainer}
open={metricsDialogOpen}
onOpenChange={setMetricsDialogOpen}
/>
</>
);
};
@@ -0,0 +1,695 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, AreaChart, Area } from "recharts";
import { DockerContainer, DockerMetrics } from "@/types/docker.types";
import { dockerService } from "@/services/dockerService";
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
interface DockerMetricsDialogProps {
container: DockerContainer | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
const timeRangeOptions = [
{ value: '60m' as TimeRange, label: '60 minutes', hours: 1 },
{ value: '1d' as TimeRange, label: '1 day', hours: 24 },
{ value: '7d' as TimeRange, label: '7 days', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: '1 month', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: '3 months', hours: 24 * 90 },
];
export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMetricsDialogProps) => {
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
const { theme } = useTheme();
const {
data: metrics = [],
isLoading,
error
} = useQuery({
queryKey: ['docker-metrics', container?.docker_id, timeRange],
queryFn: () => container ? dockerService.getContainerMetrics(container.docker_id) : Promise.resolve([]),
enabled: !!container && open,
refetchInterval: 30000
});
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const parseValueWithUnit = (value: string | number): { numeric: number; unit: string; original: string } => {
if (typeof value === 'number') {
return { numeric: value, unit: 'B', original: value.toString() };
}
const str = value.toString();
const match = str.match(/^([\d.]+)\s*([A-Za-z%]*)/);
if (match) {
const numeric = parseFloat(match[1]);
const unit = match[2] || '';
return { numeric, unit, original: str };
}
return { numeric: 0, unit: '', original: str };
};
const convertToBytes = (value: string | number): number => {
if (typeof value === 'number') return value;
const parsed = parseValueWithUnit(value);
const multipliers: { [key: string]: number } = {
'B': 1,
'KB': 1024,
'MB': 1024 * 1024,
'GB': 1024 * 1024 * 1024,
'TB': 1024 * 1024 * 1024 * 1024
};
const multiplier = multipliers[parsed.unit.toUpperCase()] || 1;
return parsed.numeric * multiplier;
};
const filterMetricsByTimeRange = (metrics: DockerMetrics[], timeRange: TimeRange): DockerMetrics[] => {
const now = new Date();
const selectedRange = timeRangeOptions.find(opt => opt.value === timeRange);
if (!selectedRange) return metrics;
const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000));
return metrics.filter(metric => {
const metricTime = new Date(metric.timestamp);
return metricTime >= cutoffTime;
});
};
const formatChartData = (metrics: DockerMetrics[]) => {
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
return filteredMetrics.slice(0, 100).reverse().map((metric, index) => {
// Parse CPU usage
const cpuUsage = typeof metric.cpu_usage === 'string' ?
parseFloat(metric.cpu_usage.replace('%', '')) :
parseFloat(metric.cpu_usage) || 0;
// Parse memory values
const ramUsedBytes = convertToBytes(metric.ram_used);
const ramTotalBytes = convertToBytes(metric.ram_total);
const ramFreeBytes = convertToBytes(metric.ram_free);
const ramUsagePercent = ramTotalBytes > 0 ? (ramUsedBytes / ramTotalBytes) * 100 : 0;
// Parse disk values
const diskUsedBytes = convertToBytes(metric.disk_used);
const diskTotalBytes = convertToBytes(metric.disk_total);
const diskFreeBytes = convertToBytes(metric.disk_free);
const diskUsagePercent = diskTotalBytes > 0 ? (diskUsedBytes / diskTotalBytes) * 100 : 0;
// Network values
const networkRxBytes = metric.network_rx_bytes || 0;
const networkTxBytes = metric.network_tx_bytes || 0;
const networkRxSpeed = metric.network_rx_speed || 0;
const networkTxSpeed = metric.network_tx_speed || 0;
return {
timestamp: new Date(metric.timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
}),
// CPU
cpuUsage: Math.round(cpuUsage * 100) / 100,
cpuCores: parseInt(metric.cpu_cores) || 0,
cpuFree: 100 - cpuUsage,
// Memory
ramUsedBytes,
ramTotalBytes,
ramFreeBytes,
ramUsed: formatBytes(ramUsedBytes),
ramTotal: formatBytes(ramTotalBytes),
ramFree: formatBytes(ramFreeBytes),
ramUsagePercent: Math.round(ramUsagePercent * 100) / 100,
// Disk
diskUsedBytes,
diskTotalBytes,
diskFreeBytes,
diskUsed: formatBytes(diskUsedBytes),
diskTotal: formatBytes(diskTotalBytes),
diskFree: formatBytes(diskFreeBytes),
diskUsagePercent: Math.round(diskUsagePercent * 100) / 100,
// Network
networkRxBytes,
networkTxBytes,
networkRx: formatBytes(networkRxBytes),
networkTx: formatBytes(networkTxBytes),
networkRxSpeed: Math.round(networkRxSpeed * 100) / 100,
networkTxSpeed: Math.round(networkTxSpeed * 100) / 100,
};
});
};
const chartData = formatChartData(metrics);
const latestMetric = chartData[chartData.length - 1];
const chartConfig = {
cpuUsage: {
label: "CPU Usage (%)",
color: theme === 'dark' ? "#3b82f6" : "#2563eb",
},
ramUsagePercent: {
label: "RAM Usage (%)",
color: theme === 'dark' ? "#10b981" : "#059669",
},
diskUsagePercent: {
label: "Disk Usage (%)",
color: theme === 'dark' ? "#f59e0b" : "#d97706",
},
networkRx: {
label: "Network RX",
color: theme === 'dark' ? "#8b5cf6" : "#7c3aed",
},
networkTx: {
label: "Network TX",
color: theme === 'dark' ? "#ef4444" : "#dc2626",
},
};
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
const MetricCard = ({ title, used, total, free, percentage, icon: Icon, color }: {
title: string;
used: string;
total: string;
free: string;
percentage: number;
icon: any;
color: string;
}) => (
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4" style={{ color }} />
<span className="text-sm font-medium">{title}</span>
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Used:</span>
<span className="font-mono">{used}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Free:</span>
<span className="font-mono">{free}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Total:</span>
<span className="font-mono">{total}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Usage:</span>
<span className="font-mono">{percentage.toFixed(1)}%</span>
</div>
</div>
</div>
);
if (!container) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-7xl max-h-[95vh] overflow-y-auto bg-background text-foreground">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
<Cpu className="h-4 w-4 text-primary" />
</div>
Container Metrics: {container.name}
</div>
<div className="flex items-center gap-2">
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
<SelectTrigger className="w-[140px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{timeRangeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</DialogTitle>
<p className="text-sm text-muted-foreground">
Docker ID: {container.docker_id} {container.hostname}
</p>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center h-96">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading metrics...</span>
</div>
) : error ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>Error loading metrics: {error.message}</p>
</div>
) : chartData.length === 0 ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>No metrics data available for this container</p>
</div>
) : (
<>
{/* Current Metrics Summary */}
{latestMetric && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title="CPU"
used={`${latestMetric.cpuUsage}%`}
total={`${latestMetric.cpuCores} cores`}
free={`${(100 - latestMetric.cpuUsage).toFixed(1)}%`}
percentage={latestMetric.cpuUsage}
icon={Cpu}
color={chartConfig.cpuUsage.color}
/>
<MetricCard
title="Memory"
used={latestMetric.ramUsed}
total={latestMetric.ramTotal}
free={latestMetric.ramFree}
percentage={latestMetric.ramUsagePercent}
icon={MemoryStick}
color={chartConfig.ramUsagePercent.color}
/>
<MetricCard
title="Disk"
used={latestMetric.diskUsed}
total={latestMetric.diskTotal}
free={latestMetric.diskFree}
percentage={latestMetric.diskUsagePercent}
icon={HardDrive}
color={chartConfig.diskUsagePercent.color}
/>
<MetricCard
title="Network"
used={`RX: ${latestMetric.networkRx}`}
total={`TX: ${latestMetric.networkTx}`}
free={`Speed: ${latestMetric.networkRxSpeed} KB/s`}
percentage={0}
icon={Network}
color={chartConfig.networkRx.color}
/>
</div>
)}
<Tabs defaultValue="cpu" className="w-full">
<TabsList className="grid w-full grid-cols-4 bg-muted">
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background">
<Cpu className="h-4 w-4" />
CPU
</TabsTrigger>
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background">
<MemoryStick className="h-4 w-4" />
Memory
</TabsTrigger>
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background">
<HardDrive className="h-4 w-4" />
Disk
</TabsTrigger>
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background">
<Network className="h-4 w-4" />
Network
</TabsTrigger>
</TabsList>
<TabsContent value="cpu" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">CPU Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Line
type="monotone"
dataKey="cpuUsage"
stroke={chartConfig.cpuUsage.color}
strokeWidth={2}
dot={{ r: 3, fill: chartConfig.cpuUsage.color }}
name="CPU Usage (%)"
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">CPU Usage vs Available</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Area
type="monotone"
dataKey="cpuFree"
stackId="1"
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="CPU Available (%)"
/>
<Area
type="monotone"
dataKey="cpuUsage"
stackId="1"
stroke={chartConfig.cpuUsage.color}
fill={chartConfig.cpuUsage.color}
fillOpacity={0.6}
name="CPU Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="memory" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Memory Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Area
type="monotone"
dataKey="ramUsagePercent"
stackId="1"
stroke={chartConfig.ramUsagePercent.color}
fill={chartConfig.ramUsagePercent.color}
fillOpacity={0.6}
name="RAM Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Memory Usage (Bytes)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
tickFormatter={(value) => formatBytes(value)}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === 'Used Memory' ? formatBytes(Number(value)) :
name === 'Total Memory' ? formatBytes(Number(value)) : value,
name
]}
/>
<Area
type="monotone"
dataKey="ramTotalBytes"
stackId="1"
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="Total Memory"
/>
<Area
type="monotone"
dataKey="ramUsedBytes"
stackId="1"
stroke={chartConfig.ramUsagePercent.color}
fill={chartConfig.ramUsagePercent.color}
fillOpacity={0.6}
name="Used Memory"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="disk" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Disk Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Area
type="monotone"
dataKey="diskUsagePercent"
stackId="1"
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name="Disk Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Disk Usage (Bytes)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
tickFormatter={(value) => formatBytes(value)}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === 'Used Disk' ? formatBytes(Number(value)) :
name === 'Total Disk' ? formatBytes(Number(value)) : value,
name
]}
/>
<Area
type="monotone"
dataKey="diskTotalBytes"
stackId="1"
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="Total Disk"
/>
<Area
type="monotone"
dataKey="diskUsedBytes"
stackId="1"
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name="Used Disk"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="network" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Network Traffic</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Line
type="monotone"
dataKey="networkRxBytes"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name="RX Bytes"
dot={{ r: 2 }}
/>
<Line
type="monotone"
dataKey="networkTxBytes"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name="TX Bytes"
dot={{ r: 2 }}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Network Speed (KB/s)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<YAxis
tick={{ fontSize: 12, fill: getAxisColor() }}
axisLine={{ stroke: getGridColor() }}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
/>
<Line
type="monotone"
dataKey="networkRxSpeed"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name="RX Speed (KB/s)"
dot={{ r: 2 }}
/>
<Line
type="monotone"
dataKey="networkTxSpeed"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name="TX Speed (KB/s)"
dot={{ r: 2 }}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</>
)}
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,102 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Container, Play, Square, AlertTriangle } from "lucide-react";
import { DockerStats } from "@/types/docker.types";
import { useTheme } from "@/contexts/ThemeContext";
interface DockerStatsCardsProps {
stats: DockerStats;
}
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
const { theme } = useTheme();
const cards = [
{
title: "Total Containers",
value: stats.total,
icon: Container,
color: "text-blue-600",
gradient: theme === 'dark'
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(59, 130, 246, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
},
{
title: "Running",
value: stats.running,
icon: Play,
color: "text-green-600",
gradient: theme === 'dark'
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(16, 185, 129, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
},
{
title: "Stopped",
value: stats.stopped,
icon: Square,
color: "text-gray-600",
gradient: theme === 'dark'
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(107, 114, 128, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
},
{
title: "Warning",
value: stats.warning,
icon: AlertTriangle,
color: "text-amber-600",
gradient: theme === 'dark'
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(245, 158, 11, 0.6) 100%)"
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #f59e0b 100%)"
},
];
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{cards.map((card) => {
const IconComponent = card.icon;
return (
<Card
key={card.title}
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative"
style={{ background: card.gradient }}
>
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 opacity-10">
<div
className="w-full h-full"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}
/>
</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white/70">
{card.title}
</CardTitle>
<div className="p-2.5 rounded-xl bg-white/20 backdrop-blur-sm shadow-sm transition-all duration-300 group-hover:scale-110">
<IconComponent className="h-4 w-4 text-white" />
</div>
</CardHeader>
<CardContent className="relative z-10">
<div className="flex items-center justify-between">
<div className="text-2xl font-bold text-white">
{card.value}
</div>
<Badge
variant="outline"
className="text-xs font-mono font-bold px-2 py-1 rounded-md bg-white/20 backdrop-blur-sm text-white border border-white/30"
>
Containers
</Badge>
</div>
</CardContent>
</Card>
);
})}
</div>
);
};
@@ -0,0 +1,48 @@
import { Badge } from "@/components/ui/badge";
interface DockerStatusBadgeProps {
status: 'running' | 'stopped' | 'warning';
}
export const DockerStatusBadge = ({ status }: DockerStatusBadgeProps) => {
const getStatusConfig = (status: string) => {
switch (status) {
case 'running':
return {
variant: 'default' as const,
className: 'bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200',
label: 'Running'
};
case 'stopped':
return {
variant: 'secondary' as const,
className: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200',
label: 'Stopped'
};
case 'warning':
return {
variant: 'destructive' as const,
className: 'bg-amber-100 text-amber-800 border-amber-200 hover:bg-amber-200',
label: 'Warning'
};
default:
return {
variant: 'outline' as const,
className: 'bg-gray-100 text-gray-600 border-gray-200',
label: 'Unknown'
};
}
};
const config = getStatusConfig(status);
return (
<Badge
variant={config.variant}
className={`${config.className} font-medium text-xs px-2 py-1`}
>
{config.label}
</Badge>
);
};
@@ -0,0 +1,23 @@
import { TableCell, TableRow } from "@/components/ui/table";
interface DockerEmptyStateProps {
searchTerm: string;
}
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
return (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<div className="text-lg font-medium">
{searchTerm ? "No containers found" : "No containers running"}
</div>
<div className="text-sm">
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
</div>
</div>
</TableCell>
</TableRow>
);
};
@@ -0,0 +1,41 @@
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
import { DockerContainer } from "@/types/docker.types";
interface DockerRowActionsProps {
container: DockerContainer;
containerStatus: 'running' | 'stopped' | 'warning';
onContainerAction: (action: string, containerId: string, containerName: string) => void;
onViewMetrics: (container: DockerContainer) => void;
}
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-popover border-border shadow-md">
<DropdownMenuItem
onClick={() => onViewMetrics(container)}
className="cursor-pointer hover:bg-muted"
>
<BarChart3 className="mr-2 h-4 w-4" />
View Metrics
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onContainerAction('view-detail', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
<Eye className="mr-2 h-4 w-4" />
View Details
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -0,0 +1,19 @@
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
export const DockerTableHeader = () => {
return (
<TableHeader>
<TableRow className="border-border bg-muted/30">
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
);
};
@@ -0,0 +1,123 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { Progress } from "@/components/ui/progress";
import { DockerContainer } from "@/types/docker.types";
import { DockerStatusBadge } from "../DockerStatusBadge";
import { DockerRowActions } from "./DockerRowActions";
import { dockerService } from "@/services/dockerService";
interface DockerTableRowProps {
container: DockerContainer;
onRowClick: (container: DockerContainer) => void;
onContainerAction: (action: string, containerId: string, containerName: string) => void;
onViewMetrics: (container: DockerContainer) => void;
}
export const DockerTableRow = ({ container, onRowClick, onContainerAction, onViewMetrics }: DockerTableRowProps) => {
const cpuPercentage = container.cpu_usage;
const memoryPercentage = Math.round((container.ram_used / container.ram_total) * 100);
const diskPercentage = Math.round((container.disk_used / container.disk_total) * 100);
const containerStatus = dockerService.getStatusFromDockerStatus(container.status);
const formatPercentage = (used: number, total: number) => {
if (total === 0) return "0%";
return `${Math.round((used / total) * 100)}%`;
};
const getUsageColor = (percentage: number) => {
if (percentage >= 90) return "text-red-500";
if (percentage >= 70) return "text-amber-500";
return "text-emerald-500";
};
const getProgressColor = (percentage: number) => {
if (percentage >= 90) return "bg-red-500";
if (percentage >= 70) return "bg-amber-500";
return "bg-emerald-500";
};
return (
<TableRow
className="hover:bg-muted/50 transition-colors border-border cursor-pointer"
onClick={() => onRowClick(container)}
>
<TableCell className="font-medium">
<div className="space-y-1">
<div className="font-semibold text-sm sm:text-base text-foreground">{container.name}</div>
<div className="text-xs sm:text-sm text-muted-foreground">
<div className="font-mono">{container.docker_id}</div>
</div>
</div>
</TableCell>
<TableCell>
<DockerStatusBadge status={containerStatus} />
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={cpuPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(cpuPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(cpuPercentage)}`}>
{cpuPercentage}%
</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={memoryPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(memoryPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(memoryPercentage)}`}>
{formatPercentage(container.ram_used, container.ram_total)}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
{dockerService.formatBytes(container.ram_used)} / {dockerService.formatBytes(container.ram_total)}
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={diskPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(diskPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(diskPercentage)}`}>
{formatPercentage(container.disk_used, container.disk_total)}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
{dockerService.formatBytes(container.disk_used)} / {dockerService.formatBytes(container.disk_total)}
</div>
</div>
</TableCell>
<TableCell>
<span className="text-xs sm:text-sm font-medium font-mono">
{dockerService.formatUptime(container.uptime)}
</span>
</TableCell>
<TableCell>
<span className="text-xs sm:text-sm text-muted-foreground">
{new Date(container.last_checked).toLocaleString()}
</span>
</TableCell>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<DockerRowActions
container={container}
containerStatus={containerStatus}
onContainerAction={onContainerAction}
onViewMetrics={onViewMetrics}
/>
</TableCell>
</TableRow>
);
};
@@ -0,0 +1,37 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, RefreshCw } from "lucide-react";
interface DockerTableSearchProps {
searchTerm: string;
onSearchChange: (value: string) => void;
onRefresh: () => void;
isLoading: boolean;
}
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
return (
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search containers..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 sm:w-64 bg-background border-border"
/>
</div>
<Button
onClick={onRefresh}
disabled={isLoading}
variant="outline"
size="default"
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
<span className="sm:inline">Refresh</span>
</Button>
</div>
);
};
@@ -0,0 +1,6 @@
export { DockerTableSearch } from './DockerTableSearch';
export { DockerTableHeader } from './DockerTableHeader';
export { DockerTableRow } from './DockerTableRow';
export { DockerRowActions } from './DockerRowActions';
export { DockerEmptyState } from './DockerEmptyState';
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -6,7 +7,7 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react';
import { Plus, X, Server } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
@@ -17,12 +18,6 @@ interface ComponentsSelectorProps {
onComponentDelete?: (componentId: string) => void;
}
const componentTypes = [
{ value: 'uptime', label: 'Uptime Service', icon: Server },
{ value: 'ssl', label: 'SSL Certificate', icon: Shield },
{ value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle },
];
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
@@ -169,7 +164,7 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<SelectTrigger>
<SelectValue placeholder="Select an uptime service" />
</SelectTrigger>
<SelectContent className="z-50 bg-white border shadow-lg">
<SelectContent>
{services.map((service) => (
<SelectItem key={service.id} value={service.id}>
<div className="flex items-center gap-2">
@@ -206,37 +201,6 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</div>
</div>
)}
<div className="mt-4">
<Label>Quick Add Templates</Label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-2">
{componentTypes.map((type) => {
const Icon = type.icon;
return (
<Button
key={type.value}
variant="outline"
size="sm"
onClick={() => {
const component: Partial<StatusPageComponentRecord> = {
name: type.label,
description: `Monitor ${type.label.toLowerCase()}`,
service_id: '',
server_id: '',
display_order: selectedComponents.length + 1,
operational_status_id: '',
};
onComponentsChange([...selectedComponents, component]);
}}
className="justify-start"
>
<Icon className="h-4 w-4 mr-2" />
{type.label}
</Button>
);
})}
</div>
</div>
</CardContent>
</Card>
);
@@ -67,13 +67,13 @@ export const CreateOperationalPageDialog = () => {
page_style: data.page_style || '',
};
console.log('Creating operational page with payload:', payload);
// console.log('Creating operational page with payload:', payload);
const createdPage = await createMutation.mutateAsync(payload);
console.log('Created page:', createdPage);
// console.log('Created page:', createdPage);
// Create components after page is created
if (selectedComponents.length > 0) {
console.log('Creating components for page:', createdPage.id);
// console.log('Creating components for page:', createdPage.id);
for (const component of selectedComponents) {
const componentPayload = {
operational_status_id: createdPage.id,
@@ -84,7 +84,7 @@ export const CreateOperationalPageDialog = () => {
display_order: component.display_order || 1,
};
console.log('Creating component with payload:', componentPayload);
// console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
}
@@ -93,7 +93,7 @@ export const CreateOperationalPageDialog = () => {
form.reset();
setSelectedComponents([]);
} catch (error) {
console.error('Error creating operational page:', error);
// console.error('Error creating operational page:', error);
}
};
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -38,8 +38,9 @@ interface EditOperationalPageDialogProps {
export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOperationalPageDialogProps) => {
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
const [existingComponents, setExistingComponents] = useState<StatusPageComponentRecord[]>([]);
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
const [componentsLoaded, setComponentsLoaded] = useState(false);
const updateMutation = useUpdateOperationalPage();
const createComponentMutation = useCreateStatusPageComponent();
const deleteComponentMutation = useDeleteStatusPageComponent();
@@ -63,28 +64,42 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
},
});
useEffect(() => {
if (page) {
form.reset({
title: page.title,
description: page.description,
slug: page.slug,
theme: page.theme,
status: page.status,
is_public: page.is_public === 'true',
logo_url: page.logo_url || '',
custom_domain: page.custom_domain || '',
custom_css: page.custom_css || '',
page_style: page.page_style || '',
});
}
}, [page, form]);
// Memoize the form reset values to prevent unnecessary re-renders
const formValues = useMemo(() => {
if (!page) return null;
return {
title: page.title,
description: page.description,
slug: page.slug,
theme: page.theme,
status: page.status,
is_public: page.is_public === 'true',
logo_url: page.logo_url || '',
custom_domain: page.custom_domain || '',
custom_css: page.custom_css || '',
page_style: page.page_style || '',
};
}, [page?.id, page?.title, page?.description, page?.slug, page?.theme, page?.status, page?.is_public, page?.logo_url, page?.custom_domain, page?.custom_css, page?.page_style]);
// Reset form when page data changes
useEffect(() => {
if (components && components.length > 0) {
console.log('Loading existing components:', components);
setExistingComponents(components);
// Convert existing components to the format expected by ComponentsSelector
if (formValues) {
form.reset(formValues);
}
}, [formValues, form]);
// Convert components to selector format and initialize state - only when dialog opens and components change
useEffect(() => {
if (!open || !page?.id || !components) {
return;
}
// Only update if components actually changed or haven't been loaded yet
const componentIds = components.map(c => c.id).sort().join(',');
const currentSelectedIds = selectedComponents.map(c => c.id).filter(Boolean).sort().join(',');
if (componentIds !== currentSelectedIds || !componentsLoaded) {
// console.log('Loading existing components:', components);
const existingComponentsForSelector = components.map(comp => ({
id: comp.id,
name: comp.name,
@@ -94,25 +109,31 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
display_order: comp.display_order,
operational_status_id: comp.operational_status_id,
}));
setSelectedComponents(existingComponentsForSelector);
} else {
setExistingComponents([]);
setComponentsLoaded(true);
}
}, [open, page?.id, components, componentsLoaded, selectedComponents]);
// Reset state when dialog closes
useEffect(() => {
if (!open) {
setComponentsLoaded(false);
setSelectedComponents([]);
}
}, [components]);
}, [open]);
const handleComponentDelete = async (componentId: string) => {
const handleComponentDelete = useCallback(async (componentId: string) => {
try {
console.log('Deleting component:', componentId);
// console.log('Deleting component:', componentId);
await deleteComponentMutation.mutateAsync(componentId);
// Update local state to remove the deleted component
setSelectedComponents(prev => prev.filter(comp => comp.id !== componentId));
setExistingComponents(prev => prev.filter(comp => comp.id !== componentId));
} catch (error) {
console.error('Error deleting component:', error);
// console.error('Error deleting component:', error);
}
};
}, [deleteComponentMutation]);
const onSubmit = async (data: FormData) => {
if (!page) return;
@@ -133,21 +154,17 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
page_style: data.page_style || '',
};
console.log('Updating operational page with payload:', payload);
// console.log('Updating operational page with payload:', payload);
await updateMutation.mutateAsync({ id: page.id, data: payload });
// Handle component changes
const currentComponentIds = existingComponents.map(c => c.id);
const currentComponentIds = components.map(c => c.id);
const newComponentsToCreate = selectedComponents.filter(comp => !comp.id);
const componentsToKeep = selectedComponents.filter(comp => comp.id && currentComponentIds.includes(comp.id));
const componentsToDelete = existingComponents.filter(comp => !selectedComponents.some(selected => selected.id === comp.id));
const componentsToDelete = components.filter(comp => !selectedComponents.some(selected => selected.id === comp.id));
// Delete removed components (only if not already deleted via handleComponentDelete)
// Delete removed components
for (const component of componentsToDelete) {
if (selectedComponents.some(selected => selected.id === component.id)) {
continue; // Skip if already handled by handleComponentDelete
}
console.log('Deleting component during save:', component.id);
// console.log('Deleting component during save:', component.id);
await deleteComponentMutation.mutateAsync(component.id);
}
@@ -162,13 +179,13 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
display_order: component.display_order || 1,
};
console.log('Creating component with payload:', componentPayload);
// console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
onOpenChange(false);
} catch (error) {
console.error('Error updating operational page:', error);
// console.error('Error updating operational page:', error);
} finally {
setIsFormSubmitting(false);
}
@@ -41,11 +41,11 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
</div>
<div>
<span className="font-medium text-muted-foreground">Public:</span>
<p className="mt-1">
<div className="mt-1">
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
{page.is_public === 'true' ? 'Yes' : 'No'}
</Badge>
</p>
</div>
</div>
<div>
<span className="font-medium text-muted-foreground">Updated:</span>
@@ -1,14 +1,17 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
import { EditOperationalPageDialog } from './EditOperationalPageDialog';
import { OperationalPageCard } from './OperationalPageCard';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { OperationalPageRecord } from '@/types/operational.types';
import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
useOperationalPages,
useDeleteOperationalPage,
} from "@/hooks/useOperationalPage";
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
import { OperationalPageCard } from "./OperationalPageCard";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { OperationalPageRecord } from "@/types/operational.types";
import { Activity, Plus, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import {
AlertDialog,
AlertDialogAction,
@@ -18,17 +21,26 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
} from "@/components/ui/alert-dialog";
export const OperationalPageContent = () => {
const navigate = useNavigate();
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
const {
data: pages,
isLoading,
error,
refetch,
isRefetching,
} = useOperationalPages();
const deleteMutation = useDeleteOperationalPage();
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(
null
);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pageToDelete, setPageToDelete] = useState<OperationalPageRecord | null>(null);
const [pageToDelete, setPageToDelete] =
useState<OperationalPageRecord | null>(null);
const handleEdit = (page: OperationalPageRecord) => {
setEditingPage(page);
@@ -37,10 +49,10 @@ export const OperationalPageContent = () => {
const handleView = (page: OperationalPageRecord) => {
if (page.custom_domain) {
window.open(`https://${page.custom_domain}`, '_blank');
window.open(`https://${page.custom_domain}`, "_blank");
} else {
// Navigate to the public status page route
window.open(`/status/${page.slug}`, '_blank');
// Navigate to the public status page route using the correct format
window.open(`/public/${page.slug}`, "_blank");
}
};
@@ -56,7 +68,7 @@ export const OperationalPageContent = () => {
setDeleteDialogOpen(false);
setPageToDelete(null);
} 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">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
<h3 className="text-lg font-semibold mb-2">
Failed to load operational pages
</h3>
<p className="text-muted-foreground mb-4">
There was an error loading your operational pages. Please try again.
</p>
@@ -82,16 +96,18 @@ export const OperationalPageContent = () => {
}
return (
<div className="container mx-auto px-4 py-8">
<div className="w-full container mx-auto px-4 py-8 ">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8 ">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
<h1 className="text-3xl font-bold tracking-tight mb-2">
Operational Pages
</h1>
<p className="text-muted-foreground">
Manage your public status pages and monitor service health
</p>
</div>
<div className="flex items-center gap-2 mt-4 sm:mt-0">
<Button
variant="outline"
@@ -99,7 +115,9 @@ export const OperationalPageContent = () => {
onClick={() => refetch()}
disabled={isRefetching}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
<RefreshCw
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
/>
Refresh
</Button>
<CreateOperationalPageDialog />
@@ -135,9 +153,12 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
<h3 className="text-lg font-semibold mb-2">
No operational pages found
</h3>
<p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your services and communicate status to your users.
Create your first operational page to start monitoring your
services and communicate status to your users.
</p>
<CreateOperationalPageDialog />
</CardContent>
@@ -167,12 +188,12 @@ export const OperationalPageContent = () => {
<span className="font-medium">Total Pages:</span> {pages.length}
</div>
<div>
<span className="font-medium">Public Pages:</span>{' '}
{pages.filter(p => p.is_public === 'true').length}
<span className="font-medium">Public Pages:</span>{" "}
{pages.filter((p) => p.is_public === "true").length}
</div>
<div>
<span className="font-medium">Operational:</span>{' '}
{pages.filter(p => p.status === 'operational').length}
<span className="font-medium">Operational:</span>{" "}
{pages.filter((p) => p.status === "operational").length}
</div>
</div>
</div>
@@ -191,7 +212,9 @@ export const OperationalPageContent = () => {
<AlertDialogHeader>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
Are you sure you want to delete "{pageToDelete?.title}"? This
action cannot be undone and will permanently remove the
operational page and all its components.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -201,11 +224,11 @@ export const OperationalPageContent = () => {
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
{deleteMutation.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
};
@@ -12,7 +12,7 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
export const PublicStatusPage = () => {
const { slug } = useParams<{ slug: string }>();
console.log('PublicStatusPage - slug from params:', slug);
// console.log('PublicStatusPage - slug from params:', slug);
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
const [lastUpdated, setLastUpdated] = useState(new Date());
@@ -51,7 +51,7 @@ export const PublicStatusPage = () => {
};
}, [page?.theme]);
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
// console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
if (loading) {
return (
@@ -19,61 +19,61 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
useEffect(() => {
const fetchPublicPage = async () => {
if (!slug) {
console.log('No slug provided');
// console.log('No slug provided');
setError('No status page slug provided');
setLoading(false);
return;
}
try {
console.log('Fetching public status page for slug:', slug);
// console.log('Fetching public status page for slug:', slug);
setLoading(true);
setError(null);
// Fetch operational page
console.log('Fetching operational pages...');
// console.log('Fetching operational pages...');
const pages = await operationalPageService.getOperationalPages();
console.log('All pages:', pages);
// console.log('All pages:', pages);
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
console.log('Found page:', foundPage);
// console.log('Found page:', foundPage);
if (!foundPage) {
console.log('Page not found or not public');
// console.log('Page not found or not public');
setError('Status page not found or not public');
setLoading(false);
return;
}
setPage(foundPage);
console.log('Page set successfully');
// console.log('Page set successfully');
// Fetch components for this page
console.log('Fetching components for page:', foundPage.id);
// console.log('Fetching components for page:', foundPage.id);
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
console.log('Components found:', pageComponents);
// console.log('Components found:', pageComponents);
setComponents(pageComponents);
// Fetch all services
console.log('Fetching all services...');
// console.log('Fetching all services...');
const allServices = await serviceService.getServices();
console.log('Services found:', allServices);
// console.log('Services found:', allServices);
setServices(allServices);
// Fetch uptime data for each component that has a service
console.log('Fetching uptime data...');
// console.log('Fetching uptime data...');
const uptimePromises = pageComponents
.filter(component => component.service_id)
.map(async (component) => {
try {
console.log('Fetching uptime for service:', component.service_id);
// console.log('Fetching uptime for service:', component.service_id);
const endDate = new Date();
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
// console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
return { serviceId: component.service_id, history };
} catch (error) {
console.error(`Error fetching uptime for service ${component.service_id}:`, error);
// console.error(`Error fetching uptime for service ${component.service_id}:`, error);
return { serviceId: component.service_id, history: [] };
}
});
@@ -84,12 +84,12 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
uptimeMap[result.serviceId] = result.history;
});
setUptimeData(uptimeMap);
console.log('Uptime data set successfully');
// console.log('Uptime data set successfully');
console.log('All data fetched successfully');
// console.log('All data fetched successfully');
} catch (err) {
console.error('Error fetching public page:', err);
// console.error('Error fetching public page:', err);
setError(`Failed to load status page: ${err}`);
} finally {
setLoading(false);
@@ -0,0 +1,460 @@
import React, { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Copy, Download, Terminal, CheckCircle, Zap, Play } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { InstallCommand } from "@/types/regional.types";
import { useToast } from "@/hooks/use-toast";
interface AddRegionalAgentDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onAgentAdded: () => void;
}
export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
open,
onOpenChange,
onAgentAdded
}) => {
const [step, setStep] = useState(1);
const [regionName, setRegionName] = useState("");
const [agentIp, setAgentIp] = useState("");
const [installCommand, setInstallCommand] = useState<InstallCommand | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
const handleCreateAgent = async (e: React.FormEvent) => {
e.preventDefault();
if (!regionName.trim() || !agentIp.trim()) return;
setIsSubmitting(true);
try {
const result = await regionalService.createRegionalService({
region_name: regionName,
agent_ip_address: agentIp,
});
setInstallCommand(result.installCommand);
setStep(2);
} catch (error) {
toast({
title: "Error",
description: "Failed to create regional agent configuration.",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
const copyToClipboard = async (text: string, description: string = "Content") => {
try {
// Try the modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
return;
}
// Fallback for older browsers or non-secure contexts (like localhost)
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
} else {
throw new Error('execCommand failed');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Show the text in a modal or alert as final fallback
const userAgent = navigator.userAgent.toLowerCase();
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
let errorMessage = "Failed to copy to clipboard.";
if (isLocalhost) {
errorMessage += " This is common in local development. Please manually copy the text.";
} else if (userAgent.includes('chrome')) {
errorMessage += " Try using HTTPS or enable clipboard permissions.";
}
toast({
title: "Copy failed",
description: errorMessage,
variant: "destructive",
});
// As a last resort, select the text for manual copying
try {
const textarea = document.querySelector('textarea[readonly]') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
}
} catch (selectError) {
// console.error('Failed to select text:', selectError);
}
}
};
const downloadScript = () => {
if (!installCommand) return;
const blob = new Blob([installCommand.bash_script], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `install-regional-agent-${installCommand.agent_id}.sh`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "Downloaded!",
description: "Installation script downloaded successfully.",
});
};
const handleComplete = () => {
onAgentAdded();
setStep(1);
setRegionName("");
setAgentIp("");
setInstallCommand(null);
onOpenChange(false);
};
const resetDialog = () => {
setStep(1);
setRegionName("");
setAgentIp("");
setInstallCommand(null);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
<DialogHeader>
<DialogTitle>Add Regional Monitoring Agent</DialogTitle>
<DialogDescription>
Deploy a regional monitoring agent with automatic one-click installation.
</DialogDescription>
</DialogHeader>
{step === 1 && (
<form onSubmit={handleCreateAgent} className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="regionName">Region Name</Label>
<Input
id="regionName"
placeholder="e.g., us-east-1, europe-west-1, asia-pacific-1"
value={regionName}
onChange={(e) => setRegionName(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="agentIp">Agent Server IP Address</Label>
<Input
id="agentIp"
placeholder="e.g., 192.168.1.100 or your-server.example.com"
value={agentIp}
onChange={(e) => setAgentIp(e.target.value)}
required
/>
</div>
</div>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Generating..." : "Generate Installation"}
</Button>
</div>
</form>
)}
{step === 2 && installCommand && (
<div className="space-y-6">
<div className="text-center space-y-2">
<CheckCircle className="h-12 w-12 text-green-600 mx-auto" />
<h3 className="text-lg font-semibold">Agent Configuration Ready!</h3>
<p className="text-muted-foreground">
One-click installation script generated with automatic configuration.
</p>
</div>
<Tabs defaultValue="oneclicK" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="oneclicK">One-Click Install</TabsTrigger>
<TabsTrigger value="details">Agent Details</TabsTrigger>
<TabsTrigger value="manual">Manual Install</TabsTrigger>
</TabsList>
<TabsContent value="oneclicK" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
One-Click Automatic Installation
</CardTitle>
<CardDescription>
Complete installation, configuration, and service startup in one command
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
<Play className="h-4 w-4" />
What this script does automatically:
</div>
<ul className="text-sm text-green-700 space-y-1 ml-6">
<li> Downloads the latest .deb package</li>
<li> Installs the regional monitoring agent</li>
<li> Creates configuration file with your settings</li>
<li> Starts and enables the service</li>
<li> Runs health checks</li>
</ul>
</div>
<div className="space-y-2">
<Label>Run this command on your target server:</Label>
<div className="relative">
<Textarea
readOnly
value={`# One-click installation command:
curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
--region-name="${regionName}" \\
--agent-id="${installCommand.agent_id}" \\
--agent-ip="${agentIp}" \\
--token="${installCommand.token}" \\
--pocketbase-url="${installCommand.api_endpoint}"`}
className="font-mono text-sm min-h-[120px] pr-12"
/>
<Button
size="sm"
variant="outline"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="flex gap-2">
<Button onClick={downloadScript} variant="outline" className="flex-1">
<Download className="mr-2 h-4 w-4" />
Download Complete Script
</Button>
<Button
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
variant="outline"
className="flex-1"
>
<Copy className="mr-2 h-4 w-4" />
Copy Full Script
</Button>
</div>
{/* Local development notice */}
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development.
You can use the "Download Complete Script" button instead or manually copy the text.
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="details" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Generated Agent Configuration</CardTitle>
<CardDescription>
These values will be automatically configured during installation
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-foreground">Agent ID</Label>
<div className="relative">
<Input
value={installCommand.agent_id}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
/>
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.agent_id, "Agent ID")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Region Name</Label>
<Input
value={regionName}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Server IP</Label>
<Input
value={agentIp}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">API Endpoint</Label>
<Input
value={installCommand.api_endpoint}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Authentication Token</Label>
<div className="relative">
<Input
value={installCommand.token}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
/>
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.token, "Authentication token")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env
</p>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="manual" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
Manual Installation Steps
</CardTitle>
<CardDescription>
Step-by-step manual installation process
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-sm space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 1: Download Package</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the amd64 architecture, please download this package. </p> <br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the arm64 architecture, please download this package. </p> <br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb
</code>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 2: Install Package</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb<br/>
sudo apt-get install -f
</code>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 3: Configure Agent</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo nano /etc/regional-check-agent/regional-check-agent.env
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf
</code>
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 4: Start Service</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl enable regional-check-agent<br/>
sudo systemctl start regional-check-agent
</code>
</div>
<div className="border-l-4 border-green-500 pl-4">
<p className="font-medium">Step 5: Verify Installation</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl status regional-check-agent<br/>
curl http://localhost:8091/health
</code>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={resetDialog}>
Add Another Agent
</Button>
<Button onClick={handleComplete}>
Complete Setup
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,151 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { MapPin, Wifi, WifiOff, MoreVertical, Trash2, Terminal, Copy } from "lucide-react";
import { RegionalService } from "@/types/regional.types";
import { formatDistanceToNow } from "date-fns";
import { useToast } from "@/hooks/use-toast";
interface RegionalAgentCardProps {
agent: RegionalService;
onDelete: () => void;
}
export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onDelete }) => {
const { toast } = useToast();
// Check if this is the default agent that cannot be removed
const isDefaultAgent = agent.agent_id === "1" || agent.region_name === "Default";
const copyAgentId = async () => {
try {
await navigator.clipboard.writeText(agent.agent_id);
toast({
title: "Copied!",
description: "Agent ID copied to clipboard.",
});
} catch (error) {
toast({
title: "Copy failed",
description: "Failed to copy agent ID.",
variant: "destructive",
});
}
};
const getConnectionStatus = () => {
if (agent.connection === 'online') {
return {
icon: <Wifi className="h-4 w-4" />,
label: 'Online',
variant: 'default' as const,
className: 'bg-green-100 text-green-800 hover:bg-green-100'
};
} else {
return {
icon: <WifiOff className="h-4 w-4" />,
label: 'Offline',
variant: 'secondary' as const,
className: 'bg-red-100 text-red-800 hover:bg-red-100'
};
}
};
const connectionStatus = getConnectionStatus();
return (
<Card className="relative">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-2">
<MapPin className="h-5 w-5 text-blue-600" />
<div>
<CardTitle className="text-lg flex items-center gap-2">
{agent.region_name}
{isDefaultAgent && (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
Default
</Badge>
)}
</CardTitle>
<CardDescription className="flex items-center gap-1 text-sm">
{agent.agent_ip_address}
</CardDescription>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={copyAgentId}>
<Copy className="mr-2 h-4 w-4" />
Copy Agent ID
</DropdownMenuItem>
{!isDefaultAgent && (
<DropdownMenuItem onClick={onDelete} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Remove Agent
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Badge
variant={connectionStatus.variant}
className={connectionStatus.className}
>
{connectionStatus.icon}
<span className="ml-1">{connectionStatus.label}</span>
</Badge>
<Badge variant="outline" className="text-xs">
{agent.status}
</Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Agent ID:</span>
<span className="font-mono text-xs">{agent.agent_id.substring(0, 12)}...</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Last Updated:</span>
<span className="text-xs">
{formatDistanceToNow(new Date(agent.updated), { addSuffix: true })}
</span>
</div>
</div>
{agent.connection === 'online' && (
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse"></div>
Active monitoring
</div>
</div>
)}
{agent.connection === 'offline' && (
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-red-600">
<div className="w-2 h-2 bg-red-600 rounded-full mr-2"></div>
Connection lost
</div>
</div>
)}
</CardContent>
</Card>
);
};
@@ -0,0 +1,181 @@
import React, { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { RegionalService } from "@/types/regional.types";
import { AddRegionalAgentDialog } from "./AddRegionalAgentDialog";
import { RegionalAgentCard } from "./RegionalAgentCard";
import { useToast } from "@/hooks/use-toast";
export const RegionalMonitoringContent = () => {
const [addDialogOpen, setAddDialogOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
const {
data: regionalServices = [],
isLoading,
error,
} = useQuery({
queryKey: ["regional-services"],
queryFn: regionalService.getRegionalServices,
refetchInterval: 30000, // Refetch every 30 seconds
});
const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
toast({
title: "Regional Agent Added",
description:
"The regional monitoring agent has been successfully configured.",
});
};
const handleDeleteAgent = async (id: string) => {
try {
await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
toast({
title: "Agent Removed",
description: "The regional monitoring agent has been removed.",
});
} catch (error) {
toast({
title: "Error",
description: "Failed to remove the regional monitoring agent.",
variant: "destructive",
});
}
};
const onlineAgents = regionalServices.filter(
(agent) => agent.connection === "online"
).length;
const totalAgents = regionalServices.length;
return (
<div className="p-6 space-y-6 ">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Regional Monitoring
</h1>
<p className="text-muted-foreground">
Manage distributed monitoring agents across different regions
</p>
</div>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Add Regional Agent
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalAgents}</div>
<p className="text-xs text-muted-foreground">
Regional monitoring agents
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Online Agents</CardTitle>
<Wifi className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Currently connected</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Offline Agents
</CardTitle>
<WifiOff className="h-4 w-4 text-red-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{totalAgents - onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Disconnected agents</p>
</CardContent>
</Card>
</div>
{/* Agents List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Regional Agents</h2>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => (
<Card key={i} className="animate-pulse">
<CardHeader>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="h-3 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded w-5/6"></div>
</div>
</CardContent>
</Card>
))}
</div>
) : regionalServices.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<p className="text-muted-foreground text-center mb-4">
Get started by adding your first regional monitoring agent to
extend your monitoring coverage.
</p>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Add First Agent
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{regionalServices.map((agent) => (
<RegionalAgentCard
key={agent.id}
agent={agent}
onDelete={() => handleDeleteAgent(agent.id)}
/>
))}
</div>
)}
</div>
<AddRegionalAgentDialog
open={addDialogOpen}
onOpenChange={setAddDialogOpen}
onAgentAdded={handleAgentAdded}
/>
</div>
);
};
@@ -0,0 +1,4 @@
export { RegionalMonitoringContent } from './RegionalMonitoringContent';
export { AddRegionalAgentDialog } from './AddRegionalAgentDialog';
export { RegionalAgentCard } from './RegionalAgentCard';
@@ -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 { Button } from "@/components/ui/button";
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
import { IncidentManagementTab } from "./IncidentManagementTab";
import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
import { useToast } from '@/hooks/use-toast';
import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog";
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog";
import { useToast } from "@/hooks/use-toast";
import {
initMaintenanceNotifications,
stopMaintenanceNotifications,
} from "@/services/maintenance/maintenanceNotificationService";
export const ScheduleIncidentContent = () => {
const { t } = useLanguage();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("maintenance");
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
// Initialize maintenance notifications when the component mounts
useEffect(() => {
console.log("Initializing maintenance notifications");
// console.log("Initializing maintenance notifications");
initMaintenanceNotifications();
// Clean up when the component unmounts
return () => {
console.log("Cleaning up maintenance notifications");
// console.log("Cleaning up maintenance notifications");
stopMaintenanceNotifications();
};
}, []);
@@ -43,65 +47,67 @@ export const ScheduleIncidentContent = () => {
const handleMaintenanceCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = refreshTrigger + 1;
console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
setRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t('success'),
description: t('maintenanceCreatedSuccess'),
title: t("success"),
description: t("maintenanceCreatedSuccess"),
});
};
const handleIncidentCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = incidentRefreshTrigger + 1;
console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
setIncidentRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t('success'),
description: t('incidentCreatedSuccess'),
title: t("success"),
description: t("incidentCreatedSuccess"),
});
};
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6">
<div className="flex flex-col gap-6">
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
<div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-foreground">
{t('scheduleIncidentManagement')}
{t("scheduleIncidentManagement")}
</h2>
<Button
<Button
className="text-primary-foreground"
onClick={handleCreateButtonClick}
>
<Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
<Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance"
? t("createMaintenanceWindow")
: t("createIncident")}
</Button>
</div>
<Tabs
defaultValue="maintenance"
<Tabs
defaultValue="maintenance"
className="w-full"
onValueChange={(value) => setActiveTab(value)}
>
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="maintenance">
<CalendarClock className="w-4 h-4 mr-2" />
{t('scheduledMaintenance')}
{t("scheduledMaintenance")}
</TabsTrigger>
<TabsTrigger value="incidents">
<AlertCircle className="w-4 h-4 mr-2" />
{t('incidentManagement')}
{t("incidentManagement")}
</TabsTrigger>
</TabsList>
<TabsContent value="maintenance" className="space-y-4">
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
</TabsContent>
<TabsContent value="incidents" className="space-y-4">
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
</TabsContent>
@@ -109,14 +115,14 @@ export const ScheduleIncidentContent = () => {
</div>
{/* Maintenance creation dialog */}
<CreateMaintenanceDialog
<CreateMaintenanceDialog
open={createMaintenanceDialogOpen}
onOpenChange={setCreateMaintenanceDialogOpen}
onMaintenanceCreated={handleMaintenanceCreated}
/>
{/* Incident creation dialog */}
<CreateIncidentDialog
<CreateIncidentDialog
open={createIncidentDialogOpen}
onOpenChange={setCreateIncidentDialogOpen}
onIncidentCreated={handleIncidentCreated}
@@ -1,16 +1,13 @@
import React, { useEffect } from 'react';
import { useLanguage } from '@/contexts/LanguageContext';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { MaintenanceTable, MaintenanceStatusChecker } from './maintenance';
import { EmptyMaintenanceState } from './maintenance/EmptyMaintenanceState';
import { OverviewCard } from './common/OverviewCard';
import { LoadingState } from '@/components/services/LoadingState';
import { Calendar, Clock, CheckCircle } from 'lucide-react';
import { useMaintenanceData } from './hooks/useMaintenanceData';
import { Skeleton } from '@/components/ui/skeleton';
import { useToast } from '@/hooks/use-toast';
import { LoadingState } from '@/components/services/LoadingState';
import { useTheme } from '@/contexts/ThemeContext';
interface ScheduledMaintenanceTabProps {
refreshTrigger?: number;
@@ -18,25 +15,24 @@ interface ScheduledMaintenanceTabProps {
export const ScheduledMaintenanceTab = ({ refreshTrigger = 0 }: ScheduledMaintenanceTabProps) => {
const { t } = useLanguage();
const { theme } = useTheme();
const { toast } = useToast();
const [manualRefresh, setManualRefresh] = React.useState(0);
// Combine the external refresh trigger with our internal one
const combinedRefreshTrigger = refreshTrigger + manualRefresh;
const {
loading,
filter,
setFilter,
maintenanceData,
overviewStats,
const {
loading,
filter,
setFilter,
maintenanceData,
overviewStats,
fetchMaintenanceData,
isEmpty,
error,
initialized
initialized,
} = useMaintenanceData({ refreshTrigger: combinedRefreshTrigger });
// Display toast when error occurs
useEffect(() => {
if (error) {
toast({
@@ -47,66 +43,91 @@ export const ScheduledMaintenanceTab = ({ refreshTrigger = 0 }: ScheduledMainten
}
}, [error, toast, t]);
// Force fetch data on initial load
useEffect(() => {
console.log("ScheduledMaintenanceTab is mounted, fetching data");
fetchMaintenanceData(true);
}, [fetchMaintenanceData]);
// Handle maintenance updates
const handleMaintenanceUpdated = () => {
console.log("Maintenance updated, refreshing data");
setManualRefresh(prev => prev + 1);
setManualRefresh((prev) => prev + 1);
};
// Handle tab changes
const handleTabChange = (value: string) => {
setFilter(value);
};
// Show full-page loading state during initial load
if (loading && !initialized) {
return <LoadingState />;
}
const gradientCard = (
title: string,
value: string | number,
icon: JSX.Element,
gradient: string
) => (
<Card
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative z-10"
style={{
background: theme === 'dark'
? `linear-gradient(135deg, rgba(65,59,55,0.8) 0%, ${gradient})`
: `linear-gradient(135deg, rgba(65,59,55,0.8) 0%, ${gradient})`,
}}
>
<div className="absolute inset-0 z-0 opacity-10">
<div
className="w-full h-full"
style={{
backgroundImage: `linear-gradient(#000 1px, transparent 1px),
linear-gradient(90deg, #fff 1px, transparent 1px)`,
backgroundSize: '20px 20px',
}}
/>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{title}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-4xl font-bold text-white">{value}</span>
<div className="rounded-full p-3 bg-white/25 backdrop-blur-sm">
{icon}
</div>
</CardContent>
</Card>
);
return (
<>
{/* Status checker for automatic updates */}
<MaintenanceStatusChecker
<MaintenanceStatusChecker
maintenanceData={maintenanceData}
onStatusUpdated={handleMaintenanceUpdated}
/>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<OverviewCard
title={t('upcomingMaintenance')}
value={loading ? '...' : overviewStats.upcoming}
icon={<Calendar className="h-5 w-5 text-white" />}
isLoading={loading}
color="blue"
/>
<OverviewCard
title={t('ongoingMaintenance')}
value={loading ? '...' : overviewStats.ongoing}
icon={<Clock className="h-5 w-5 text-white" />}
isLoading={loading}
color="amber"
/>
<OverviewCard
title={t('completedMaintenance')}
value={loading ? '...' : overviewStats.completed}
icon={<CheckCircle className="h-5 w-5 text-white" />}
isLoading={loading}
color="green"
/>
<OverviewCard
title={t('totalScheduledHours')}
value={loading ? '...' : `${overviewStats.totalDuration}h`}
icon={<Clock className="h-5 w-5 text-white" />}
isLoading={loading}
color="purple"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 w-full">
{gradientCard(
t('upcomingMaintenance'),
loading ? '...' : overviewStats.upcoming,
<Calendar className="h-6 w-6 text-white" />,
'rgba(59,130,246,0.6)'
)}
{gradientCard(
t('ongoingMaintenance'),
loading ? '...' : overviewStats.ongoing,
<Clock className="h-6 w-6 text-white" />,
'rgba(251,191,36,0.6)'
)}
{gradientCard(
t('completedMaintenance'),
loading ? '...' : overviewStats.completed,
<CheckCircle className="h-6 w-6 text-white" />,
'rgba(34,197,94,0.6)'
)}
{gradientCard(
t('totalScheduledHours'),
loading ? '...' : `${overviewStats.totalDuration}h`,
<Clock className="h-6 w-6 text-white" />,
'rgba(139,92,246,0.6)'
)}
</div>
<Card>
@@ -117,10 +138,10 @@ export const ScheduledMaintenanceTab = ({ refreshTrigger = 0 }: ScheduledMainten
</CardDescription>
</CardHeader>
<CardContent>
<Tabs
defaultValue="upcoming"
value={filter}
className="w-full"
<Tabs
defaultValue="upcoming"
value={filter}
className="w-full"
onValueChange={handleTabChange}
>
<TabsList className="mb-6">
@@ -129,24 +150,24 @@ export const ScheduledMaintenanceTab = ({ refreshTrigger = 0 }: ScheduledMainten
<TabsTrigger value="completed">{t('completedMaintenance')}</TabsTrigger>
</TabsList>
<TabsContent value="upcoming" className="space-y-4">
<MaintenanceTable
<TabsContent value="upcoming">
<MaintenanceTable
data={maintenanceData}
isLoading={loading && initialized}
onMaintenanceUpdated={handleMaintenanceUpdated}
/>
</TabsContent>
<TabsContent value="ongoing" className="space-y-4">
<MaintenanceTable
<TabsContent value="ongoing">
<MaintenanceTable
data={maintenanceData}
isLoading={loading && initialized}
onMaintenanceUpdated={handleMaintenanceUpdated}
/>
</TabsContent>
<TabsContent value="completed" className="space-y-4">
<MaintenanceTable
<TabsContent value="completed">
<MaintenanceTable
data={maintenanceData}
isLoading={loading && initialized}
onMaintenanceUpdated={handleMaintenanceUpdated}
@@ -18,6 +18,7 @@ interface OverviewCardProps {
valueClassName?: string;
isLoading?: boolean;
color?: string;
gradient?: string;
}
export const OverviewCard = ({
@@ -30,11 +31,15 @@ export const OverviewCard = ({
valueClassName,
isLoading = false,
color = "blue",
gradient,
}: OverviewCardProps) => {
const { theme } = useTheme();
// Map color prop to gradient colors
const getGradientBackground = () => {
if (gradient) {
return gradient;
}
const colors = {
blue: theme === 'dark'
? "linear-gradient(135deg, rgba(25, 118, 210, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)"
@@ -22,13 +22,13 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
const fetchIncidentData = useCallback(async (force = false) => {
// Skip if already fetching
if (isFetchingRef.current) {
console.log('Already fetching data, skipping additional request');
// console.log('Already fetching data, skipping additional request');
return;
}
// Skip if not forced and already initialized
if (initialized && !force) {
console.log('Data already initialized and no force refresh, skipping fetch');
// console.log('Data already initialized and no force refresh, skipping fetch');
return;
}
@@ -46,22 +46,22 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
setError(null);
try {
console.log(`Fetching incident data (force=${force})`);
// console.log(`Fetching incident data (force=${force})`);
const allIncidents = await incidentService.getAllIncidents(force);
if (Array.isArray(allIncidents)) {
setIncidents(allIncidents);
console.log(`Successfully set ${allIncidents.length} incidents to state`);
// console.log(`Successfully set ${allIncidents.length} incidents to state`);
} else {
setIncidents([]);
console.warn('No incidents returned from service');
// console.warn('No incidents returned from service');
}
setInitialized(true);
setLoading(false);
setIsRefreshing(false);
} catch (error) {
console.error('Error fetching incident data:', error);
// console.error('Error fetching incident data:', error);
setError('Failed to load incident data. Please try again later.');
setIncidents([]);
setInitialized(true);
@@ -79,7 +79,7 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
useEffect(() => {
// Skip if the refresh trigger hasn't changed (prevents duplicate effect calls)
if (refreshTrigger === lastRefreshTriggerRef.current && initialized) {
console.log('Refresh trigger unchanged, skipping fetch');
// console.log('Refresh trigger unchanged, skipping fetch');
return;
}
@@ -90,7 +90,7 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
const abortController = new AbortController();
let isMounted = true;
console.log(`useIncidentData effect running, refreshTrigger: ${refreshTrigger}`);
// console.log(`useIncidentData effect running, refreshTrigger: ${refreshTrigger}`);
// Use a longer delay to ensure we don't trigger too many API calls
const fetchTimer = setTimeout(() => {
@@ -101,7 +101,7 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
// Cleanup function to abort any in-flight requests and clear timers
return () => {
console.log('Cleaning up incident data fetch effect');
// console.log('Cleaning up incident data fetch effect');
isMounted = false;
clearTimeout(fetchTimer);
abortController.abort();
@@ -112,7 +112,7 @@ export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) =>
const incidentData = useMemo(() => {
if (!initialized || incidents.length === 0) return [];
console.log(`Filtering incidents by: ${filter}`);
// console.log(`Filtering incidents by: ${filter}`);
if (filter === "unresolved") {
return incidents.filter(item => {
@@ -3,6 +3,7 @@ import React from 'react';
import { AlertCircle, CheckCircle, Clock, AlertTriangle, Flag } from 'lucide-react';
import { useLanguage } from '@/contexts/LanguageContext';
import { OverviewCard } from '../common/OverviewCard';
import { useTheme } from '@/contexts/ThemeContext';
interface OverviewStatsProps {
unresolved: number;
@@ -24,6 +25,7 @@ export const OverviewCards: React.FC<OverviewCardsProps> = ({
initialized
}) => {
const { t } = useLanguage();
const { theme } = useTheme();
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
@@ -32,35 +34,55 @@ export const OverviewCards: React.FC<OverviewCardsProps> = ({
value={overviewStats.unresolved.toString()}
icon={<AlertCircle className="h-5 w-5 text-white" />}
isLoading={loading && initialized}
color="red"
gradient={
theme === "dark"
? "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)"
: "linear-gradient(135deg, #4b3b37 0%, rgba(239, 83, 80, 0.6) 100%)"
}
/>
<OverviewCard
title={t('criticalIssues')}
value={overviewStats.critical.toString()}
icon={<AlertTriangle className="h-5 w-5 text-white" />}
isLoading={loading && initialized}
color="amber"
gradient={
theme === "dark"
? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)"
: "linear-gradient(135deg, #4b3b37 0%, rgba(255, 183, 77, 0.6) 100%)"
}
/>
<OverviewCard
title={t('highPriority')}
value={overviewStats.highPriority.toString()}
icon={<Flag className="h-5 w-5 text-white" />}
isLoading={loading && initialized}
color="orange"
gradient={
theme === "dark"
? "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)"
: "linear-gradient(135deg, #4b3b37 0%, rgba(255, 109, 0, 0.6) 100%)"
}
/>
<OverviewCard
title={t('resolvedIncidents')}
value={overviewStats.resolved.toString()}
icon={<CheckCircle className="h-5 w-5 text-white" />}
isLoading={loading && initialized}
color="green"
gradient={
theme === "dark"
? "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)"
: "linear-gradient(135deg, #4b3b37 0%, rgba(102, 187, 106, 0.6) 100%)"
}
/>
<OverviewCard
title={t('avgResolutionTime')}
value={overviewStats.avgResolutionTime}
icon={<Clock className="h-5 w-5 text-white" />}
isLoading={loading && initialized}
color="blue"
gradient={
theme === "dark"
? "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)"
: "linear-gradient(135deg, #4b3b37 0%, rgba(66, 165, 245, 0.6) 100%)"
}
/>
</div>
);
@@ -25,19 +25,20 @@ export const IncidentDetailContent = ({
onClose,
assignedUser
}: IncidentDetailContentProps) => {
// Fetch assigned user details if one wasn't provided and there's an assigned_to field
// Fetch assigned user details if none was provided; prefer server field
const assigneeId = incident?.assigned_users || incident?.assigned_to;
const { data: fetchedUser } = useQuery({
queryKey: ['user', incident?.assigned_to],
queryKey: ['user', assigneeId],
queryFn: async () => {
if (!incident?.assigned_to) return null;
if (!assigneeId) return null;
try {
return await userService.getUser(incident.assigned_to);
return await userService.getUser(assigneeId);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!incident?.assigned_to && !assignedUser,
enabled: !!assigneeId && !assignedUser,
staleTime: 300000 // Cache for 5 minutes
});
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
onOpenChange,
incident
}: IncidentDetailDialogProps) => {
// Fetch user data for assigned_to field
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to)
const { data: users = [] } = useQuery({
queryKey: ['users'],
queryFn: async () => {
@@ -25,12 +25,12 @@ export const IncidentDetailDialog = ({
return Array.isArray(usersList) ? usersList : [];
},
staleTime: 300000, // Cache for 5 minutes
enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
enabled: !!(incident?.assigned_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open
});
// Find the assigned user
const assignedUser = incident?.assigned_to
? users.find(user => user.id === incident.assigned_to)
// Find the assigned user (prefer assigned_users, fallback to assigned_to)
const assignedUser = (incident?.assigned_users || incident?.assigned_to)
? users.find(user => user.id === (incident?.assigned_users || incident?.assigned_to))
: null;
if (!incident) return null;
@@ -23,19 +23,19 @@ export {
// Legacy component - keeping this for backward compatibility with other imports
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
// Fetch assigned user details if there's an assigned_to field
// Fetch assigned user details if there's an assigned field (prefer assigned_users, fallback to assigned_to)
const { data: assignedUser } = useQuery({
queryKey: ['user', incident?.assigned_to],
queryKey: ['user', incident?.assigned_users || incident?.assigned_to],
queryFn: async () => {
if (!incident?.assigned_to) return null;
if (!(incident?.assigned_users || incident?.assigned_to)) return null;
try {
return await userService.getUser(incident.assigned_to);
return await userService.getUser(incident?.assigned_users || incident?.assigned_to);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!incident?.assigned_to,
enabled: !!(incident?.assigned_users || incident?.assigned_to),
staleTime: 300000 // Cache for 5 minutes
});
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : incident.assigned_to ? (
<span>{incident.assigned_to}</span>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span>
)}
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : incident.assigned_to ? (
<span>{incident.assigned_to}</span>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span>
)}
@@ -40,10 +40,10 @@ export const IncidentBasicFields: React.FC = () => {
queryFn: async () => {
try {
const usersList = await userService.getUsers();
console.log("Fetched users for incident assignment:", usersList);
// console.log("Fetched users for incident assignment:", usersList);
return Array.isArray(usersList) ? usersList : [];
} catch (error) {
console.error("Failed to fetch users:", error);
// console.error("Failed to fetch users:", error);
return [];
}
},
@@ -53,7 +53,7 @@ export const IncidentBasicFields: React.FC = () => {
// Add user to assigned_to
const addUser = (userId: string) => {
// For now, we're using a single user assignment
console.log("Setting user ID in form:", userId);
// console.log("Setting user ID in form:", userId);
form.setValue('assigned_to', userId, { shouldValidate: true, shouldDirty: true });
setSelectedUserIds([userId]);
};
@@ -68,7 +68,7 @@ export const IncidentBasicFields: React.FC = () => {
const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
// Function to get user initials from name
const getUserInitials = (user: any): string => {
const getUserInitials = (user: { full_name?: string; username: string }): string => {
if (user.full_name) {
const nameParts = user.full_name.split(' ');
if (nameParts.length > 1) {
@@ -25,7 +25,7 @@ export const useIncidentEditForm = (
impact: (incident.impact?.toLowerCase() || 'minor') as any,
priority: (incident.priority?.toLowerCase() || 'medium') as any,
service_id: incident.service_id || '',
assigned_to: incident.assigned_to || '',
assigned_to: incident.assigned_users || incident.assigned_to || '',
root_cause: incident.root_cause || '',
resolution_steps: incident.resolution_steps || '',
lessons_learned: incident.lessons_learned || '',
@@ -45,11 +45,14 @@ export const useIncidentEditForm = (
impact: data.impact,
priority: data.priority,
service_id: data.service_id,
assigned_to: data.assigned_to, // This is the user ID from the form
...(data.assigned_to
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
: {}),
root_cause: data.root_cause,
resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned,
});
toast({
title: t('incidentUpdated'),
@@ -57,6 +57,7 @@ export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
priority: data.priority,
service_id: data.service_id,
assigned_to: data.assigned_to,
assigned_users: data.assigned_to, // map to server field
root_cause: data.root_cause,
resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned,
@@ -86,7 +86,7 @@ export const IncidentTableRow = memo(({
</Badge>
</TableCell>
<TableCell>
<AssignedUserCell userId={localItem.assigned_to} />
<AssignedUserCell userId={localItem.assigned_users || localItem.assigned_to} />
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-end items-center space-x-2">
@@ -27,7 +27,7 @@ export const MaintenanceNotificationSettingsField = () => {
try {
setIsLoading(true);
const channels = await alertConfigService.getAlertConfigurations();
console.log("Fetched notification channels for form:", channels);
// console.log("Fetched notification channels for form:", channels);
// Only show enabled channels
const enabledChannels = channels.filter(channel => channel.enabled);
@@ -38,18 +38,18 @@ export const MaintenanceNotificationSettingsField = () => {
const currentChannel = getValues('notification_channel_id');
const shouldNotify = getValues('notify_subscribers');
console.log("Current notification values:", {
currentChannel,
shouldNotify,
availableChannels: enabledChannels.length
});
// console.log("Current notification values:", {
// currentChannel,
// shouldNotify,
// availableChannels: enabledChannels.length
// });
if (shouldNotify && (!currentChannel || currentChannel === 'none') && enabledChannels.length > 0) {
console.log("Setting default notification channel:", enabledChannels[0].id);
// console.log("Setting default notification channel:", enabledChannels[0].id);
setValue('notification_channel_id', enabledChannels[0].id);
}
} catch (error) {
console.error('Error fetching notification channels:', error);
// console.error('Error fetching notification channels:', error);
toast({
title: t('error'),
description: t('errorFetchingNotificationChannels'),
@@ -64,12 +64,12 @@ export const MaintenanceNotificationSettingsField = () => {
}, [t, toast, setValue, getValues]);
// Log value changes for debugging
useEffect(() => {
console.log("Current notification settings:", {
channel_id: getValues('notification_channel_id'),
notify: notifySubscribers
});
}, [notifySubscribers, notificationChannelId, getValues]);
// useEffect(() => {
// console.log("Current notification settings:", {
// channel_id: getValues('notification_channel_id'),
// notify: notifySubscribers
// });
// }, [notifySubscribers, notificationChannelId, getValues]);
return (
<div className="space-y-6">
@@ -98,7 +98,7 @@ export const MaintenanceNotificationSettingsField = () => {
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
console.log("Notification toggle changed to:", checked);
// console.log("Notification toggle changed to:", checked);
// If notifications are disabled, also clear the notification channel
if (!checked) {
setValue('notification_channel_id', '');
@@ -120,10 +120,10 @@ export const MaintenanceNotificationSettingsField = () => {
// Make sure to handle both empty string and "none" as special cases
const displayValue = field.value || "";
console.log("Rendering notification channel field with value:", {
fieldValue: field.value,
displayValue
});
// console.log("Rendering notification channel field with value:", {
// fieldValue: field.value,
// displayValue
// });
return (
<FormItem>
@@ -135,7 +135,7 @@ export const MaintenanceNotificationSettingsField = () => {
<Select
value={displayValue}
onValueChange={(value) => {
console.log("Setting notification channel to:", value);
// console.log("Setting notification channel to:", value);
field.onChange(value === "none" ? "" : value);
}}
disabled={!notifySubscribers}
@@ -31,17 +31,17 @@ export const AssignedUsersField = () => {
// Ensure assigned_users is initialized as an array
useEffect(() => {
const currentValue = form.getValues('assigned_users');
console.log("Initial assigned_users value:", currentValue);
// console.log("Initial assigned_users value:", currentValue);
// Initialize as empty array if no value or invalid value
if (!currentValue || !Array.isArray(currentValue)) {
console.log("Initializing assigned_users as empty array");
// console.log("Initializing assigned_users as empty array");
form.setValue('assigned_users', [], { shouldValidate: false, shouldDirty: true });
}
}, [form]);
console.log("Current form values:", form.getValues());
console.log("Current assigned_users:", form.getValues('assigned_users'));
//console.log("Current form values:", form.getValues());
//console.log("Current assigned_users:", form.getValues('assigned_users'));
// Fetch users for the assignment dropdown
const { data: users = [], isLoading } = useQuery({
@@ -49,10 +49,10 @@ export const AssignedUsersField = () => {
queryFn: async () => {
try {
const usersList = await userService.getUsers();
console.log("Fetched users for assignment:", usersList);
// console.log("Fetched users for assignment:", usersList);
return Array.isArray(usersList) ? usersList : [];
} catch (error) {
console.error("Failed to fetch users:", error);
// console.error("Failed to fetch users:", error);
return [];
}
},
@@ -64,7 +64,7 @@ export const AssignedUsersField = () => {
? form.watch('assigned_users')
: [];
console.log("Selected user IDs:", selectedUserIds);
// console.log("Selected user IDs:", selectedUserIds);
// Function to add a user
const addUser = (userId: string) => {
@@ -73,7 +73,7 @@ export const AssignedUsersField = () => {
: [];
if (!currentValues.includes(userId)) {
console.log("Adding user:", userId);
// console.log("Adding user:", userId);
form.setValue('assigned_users', [...currentValues, userId], { shouldValidate: true, shouldDirty: true });
}
};
@@ -84,7 +84,7 @@ export const AssignedUsersField = () => {
? [...form.getValues('assigned_users')]
: [];
console.log("Removing user:", userId);
// console.log("Removing user:", userId);
form.setValue(
'assigned_users',
currentValues.filter(id => id !== userId),
@@ -94,7 +94,7 @@ export const AssignedUsersField = () => {
// Get selected users data
const selectedUsers = users.filter(user => selectedUserIds.includes(user.id));
console.log("Matched selected users:", selectedUsers);
// console.log("Matched selected users:", selectedUsers);
// Function to get user initials from name
const getUserInitials = (user: any): string => {
@@ -0,0 +1,179 @@
import React, { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Server } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentEndpoint } from "@/lib/pocketbase";
import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
import { OneClickInstallTab } from "./OneClickInstallTab";
import { DockerOneClickTab } from "./DockerOneClickTab";
import { ManualInstallTab } from "./ManualInstallTab";
interface AddServerAgentDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onAgentAdded: () => void;
}
export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
open,
onOpenChange,
onAgentAdded,
}) => {
const { toast } = useToast();
const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState("configure");
// Get current PocketBase URL
const currentPocketBaseUrl = getCurrentEndpoint();
// Form state
const [formData, setFormData] = useState({
serverName: "",
description: "",
osType: "",
checkInterval: "60",
retryAttempt: "3",
dockerEnabled: false,
notificationEnabled: true,
});
// Generated server token and agent ID
const [serverToken] = useState(() =>
`srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
);
const [serverId] = useState(() =>
`agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
if (!formData.serverName || !formData.osType) {
toast({
title: "Validation Error",
description: "Please fill in all required fields.",
variant: "destructive",
});
return;
}
setIsSubmitting(true);
try {
// Here you would typically create the server monitoring configuration
// For now, we'll simulate the process
await new Promise(resolve => setTimeout(resolve, 1500));
toast({
title: "Server Agent Created",
description: `${formData.serverName} monitoring agent has been configured successfully.`,
});
// Switch to one-click install tab after successful creation
setActiveTab("one-click");
onAgentAdded();
} catch (error) {
toast({
title: "Error",
description: "Failed to create server monitoring agent.",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
const handleDialogClose = () => {
// Reset form and tab when dialog closes
setActiveTab("configure");
setFormData({
serverName: "",
description: "",
osType: "",
checkInterval: "60",
retryAttempt: "3",
dockerEnabled: false,
notificationEnabled: true,
});
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleDialogClose}>
<DialogContent className="sm:max-w-[900px] max-w-[95vw] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Add Server Monitoring Agent
</DialogTitle>
<DialogDescription>
Configure a new server monitoring agent to track system metrics and performance.
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
</TabsList>
<TabsContent value="configure" className="space-y-6">
<ServerAgentConfigForm
formData={formData}
setFormData={setFormData}
serverId={serverId}
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
isSubmitting={isSubmitting}
onSubmit={handleSubmit}
/>
</TabsContent>
<TabsContent value="one-click" className="space-y-6">
<OneClickInstallTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="docker-one-click" className="space-y-6">
<DockerOneClickTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="manual" className="space-y-6">
<ManualInstallTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,176 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download, Container } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface DockerOneClickTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getDockerOneClickCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
return `curl -L -o server-docker-agent.sh "${scriptUrl}"
chmod +x server-docker-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
sudo -E bash ./server-docker-agent.sh`;
};
const getDirectDockerCommand = () => {
return `docker run -d \\
--name monitoring-agent \\
--restart unless-stopped \\
-p 8081:8081 \\
--group-add 999 \\
-e AGENT_ID="${serverId}" \\
-e SERVER_NAME="${formData.serverName}" \\
-e SERVER_TOKEN="${serverToken}" \\
-e POCKETBASE_URL="${currentPocketBaseUrl}" \\
-e POCKETBASE_ENABLED=true \\
-v /proc:/host/proc:ro \\
-v /etc:/host/etc:ro \\
-v /sys:/host/sys:ro \\
-v /:/host/root:ro \\
-v /var/run:/host/var/run:ro \\
-v /dev:/host/dev:ro \\
-v /var/run/docker.sock:/var/run/docker.sock:ro \\
-v monitoring_data:/var/lib/monitoring-agent \\
-v monitoring_logs:/var/log/monitoring-agent \\
operacle/checkcle-server-agent:latest`;
};
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy one-click command button clicked');
const command = getDockerOneClickCommand();
// console.log('Copying one-click command:', command);
await copyToClipboard(command);
};
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy docker command button clicked');
const command = getDirectDockerCommand();
// console.log('Copying docker command:', command);
await copyToClipboard(command);
};
return (
<div className="space-y-6">
{/* One-Click Docker Installation */}
<Card className="border-blue-500/20 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" />
Docker One-Click Install
</CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300">
Automated Docker container installation with system monitoring capabilities
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
<div className="relative">
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
<code>{getDockerOneClickCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-blue-50 dark:bg-blue-950/50 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-400"
onClick={handleCopyOneClickCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
<p className="font-medium mb-1">This script will automatically:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Download and setup the Docker monitoring agent</li>
<li>Configure all required environment variables</li>
<li>Start the container with proper system access</li>
<li>Setup monitoring data persistence</li>
</ol>
</div>
</CardContent>
</Card>
{/* Direct Docker Run Command */}
<Card className="border-gray-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Container className="h-5 w-5" />
Direct Docker Run Command
</CardTitle>
<CardDescription>
If you prefer to run the Docker container directly without the script
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Docker Run Command</Label>
<div className="relative">
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{getDirectDockerCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={handleCopyDockerCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
<p className="font-medium mb-1">Prerequisites for direct Docker run:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Docker must be installed and running</li>
<li>The operacle/checkcle-server-agent image must be available</li>
<li>Run as root or with sudo privileges</li>
</ol>
</div>
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</div>
);
};
@@ -0,0 +1,671 @@
import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { useToast } from "@/hooks/use-toast";
import { pb } from "@/lib/pocketbase";
import { Server } from "@/types/server.types";
import { RefreshCw, X } from "lucide-react";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
interface EditServerDialogProps {
server: Server | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onServerUpdated: () => void;
}
interface ServerFormData {
name: string;
check_interval: number;
max_retries: number;
docker_monitoring: boolean;
notification_enabled: boolean;
notification_channels: string[]; // Changed to array for multiple selections
threshold_id: string;
template_id: string;
}
interface ThresholdFormData {
cpu_threshold: number;
ram_threshold: number;
disk_threshold: number;
network_threshold: number;
}
export const EditServerDialog: React.FC<EditServerDialogProps> = ({
server,
open,
onOpenChange,
onServerUpdated,
}) => {
const [formData, setFormData] = useState<ServerFormData>({
name: "",
check_interval: 60,
max_retries: 3,
docker_monitoring: false,
notification_enabled: false,
notification_channels: [], // Changed to array
threshold_id: "none",
template_id: "none",
});
const [thresholdFormData, setThresholdFormData] = useState<ThresholdFormData>({
cpu_threshold: 80,
ram_threshold: 80,
disk_threshold: 80,
network_threshold: 80,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [templates, setTemplates] = useState<ServerNotificationTemplate[]>([]);
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<ServerNotificationTemplate | null>(null);
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
const [loadingTemplates, setLoadingTemplates] = useState(false);
const [loadingThresholds, setLoadingThresholds] = useState(false);
const { toast } = useToast();
// Initialize form data when server changes
useEffect(() => {
if (server) {
// Parse comma-separated notification_id into array
const notificationChannels = server.notification_id
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
: [];
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
max_retries: server.max_retries || 3,
docker_monitoring: server.docker === "true",
notification_enabled: server.notification_status === true,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
});
}
}, [server]);
// Load data when dialog opens
useEffect(() => {
if (open) {
loadAlertConfigurations();
loadTemplates();
loadThresholds();
}
}, [open]);
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id
useEffect(() => {
if (server && server.threshold_id && thresholds.length > 0) {
const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
if (existingThreshold) {
setSelectedThreshold(existingThreshold);
// Handle the API response format with proper field names and type conversion
setThresholdFormData({
cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80,
ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
});
}
}
}, [server, thresholds]);
// Update selected template when form data or templates change
useEffect(() => {
if (formData.template_id && formData.template_id !== "none" && templates.length > 0) {
const template = templates.find(t => t.id === formData.template_id);
setSelectedTemplate(template || null);
} else {
setSelectedTemplate(null);
}
}, [formData.template_id, templates]);
// Update selected threshold when threshold_id changes in form
useEffect(() => {
if (formData.threshold_id && formData.threshold_id !== "none" && thresholds.length > 0) {
const threshold = thresholds.find(t => t.id === formData.threshold_id);
setSelectedThreshold(threshold || null);
if (threshold) {
// Handle the API response format with proper field names and type conversion
setThresholdFormData({
cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80,
ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80,
disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
network_threshold: parseInt(String(threshold.network_threshold)) || 80,
});
}
} else if (formData.threshold_id === "none") {
setSelectedThreshold(null);
setThresholdFormData({
cpu_threshold: 80,
ram_threshold: 80,
disk_threshold: 80,
network_threshold: 80,
});
}
}, [formData.threshold_id, thresholds]);
const loadAlertConfigurations = async () => {
try {
setLoadingAlertConfigs(true);
const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs);
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to load notification channels",
});
} finally {
setLoadingAlertConfigs(false);
}
};
const loadTemplates = async () => {
try {
setLoadingTemplates(true);
const templateList = await templateService.getTemplates('server');
// Cast to ServerNotificationTemplate[] since we know we're getting server templates
setTemplates(templateList as ServerNotificationTemplate[]);
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to load templates",
});
} finally {
setLoadingTemplates(false);
}
};
const loadThresholds = async () => {
try {
setLoadingThresholds(true);
const thresholdList = await serverThresholdService.getServerThresholds();
setThresholds(thresholdList);
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to load server thresholds",
});
} finally {
setLoadingThresholds(false);
}
};
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
setFormData(prev => ({
...prev,
notification_channels: checked
? [...prev.notification_channels, channelId]
: prev.notification_channels.filter(id => id !== channelId)
}));
};
const removeNotificationChannel = (channelId: string) => {
setFormData(prev => ({
...prev,
notification_channels: prev.notification_channels.filter(id => id !== channelId)
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!server || isSubmitting) return;
try {
setIsSubmitting(true);
// Convert notification channels array to comma-separated string
const notificationChannelsString = formData.notification_channels.join(',');
const updateData = {
name: formData.name,
check_interval: formData.check_interval,
max_retries: formData.max_retries,
docker: formData.docker_monitoring ? "true" : "false",
notification_status: formData.notification_enabled,
notification_id: notificationChannelsString,
threshold_id: formData.threshold_id !== "none" ? formData.threshold_id : "",
template_id: formData.template_id !== "none" ? formData.template_id : "",
updated: new Date().toISOString(),
};
await pb.collection('servers').update(server.id, updateData);
// Update threshold if a threshold is selected and values have been modified
if (selectedThreshold && formData.threshold_id !== "none") {
try {
const updateThresholdData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold,
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
} catch (error) {
toast({
variant: "destructive",
title: "Warning",
description: "Server updated but failed to update threshold values.",
});
}
}
toast({
title: "Server updated",
description: `${formData.name} has been updated successfully.`,
});
onServerUpdated();
onOpenChange(false);
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to update server. Please try again.",
});
} finally {
setIsSubmitting(false);
}
};
const handleCancel = () => {
if (server) {
const notificationChannels = server.notification_id
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
: [];
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
max_retries: server.max_retries || 3,
docker_monitoring: server.docker === "true",
notification_enabled: server.notification_status === true,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
});
}
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Server Configuration</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="serverName">Server Name *</Label>
<Input
id="serverName"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Enter server name"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="checkInterval">Check Interval</Label>
<Select
value={formData.check_interval.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, check_interval: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
<SelectItem value="30">30 seconds</SelectItem>
<SelectItem value="60">1 minute</SelectItem>
<SelectItem value="120">2 minutes</SelectItem>
<SelectItem value="300">5 minutes</SelectItem>
<SelectItem value="600">10 minutes</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="maxRetries">Max Retries</Label>
<Select
value={formData.max_retries.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue placeholder="Select max retries" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 retry</SelectItem>
<SelectItem value="2">2 retries</SelectItem>
<SelectItem value="3">3 retries</SelectItem>
<SelectItem value="5">5 retries</SelectItem>
<SelectItem value="10">10 retries</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="dockerMonitoring">Docker Monitoring</Label>
<div className="flex items-center space-x-2">
<Switch
id="dockerMonitoring"
checked={formData.docker_monitoring}
onCheckedChange={(checked) => setFormData(prev => ({
...prev,
docker_monitoring: checked
}))}
/>
<Label htmlFor="dockerMonitoring" className="text-sm text-muted-foreground">
{formData.docker_monitoring ? "Enabled" : "Disabled"}
</Label>
</div>
</div>
</div>
{/* Notification Status Toggle */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Switch
id="notificationEnabled"
checked={formData.notification_enabled}
onCheckedChange={(checked) => setFormData(prev => ({
...prev,
notification_enabled: checked
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
}))}
/>
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
</div>
{/* Expanded Notification Settings */}
{formData.notification_enabled && (
<Card className="border-l-4 border-l-blue-500">
<CardHeader>
<CardTitle className="text-lg">Notification Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Multiple Notification Channels Selection */}
<div className="space-y-2">
<Label>Notification Channels</Label>
<div className="space-y-2 max-h-40 overflow-y-auto border rounded-md p-3">
{loadingAlertConfigs ? (
<div className="text-sm text-muted-foreground">Loading channels...</div>
) : alertConfigs.length > 0 ? (
alertConfigs.map((config) => (
<div key={config.id} className="flex items-center space-x-2">
<Checkbox
id={`channel-${config.id}`}
checked={formData.notification_channels.includes(config.id || "")}
onCheckedChange={(checked) =>
handleNotificationChannelToggle(config.id || "", checked as boolean)
}
/>
<Label
htmlFor={`channel-${config.id}`}
className="flex-1 text-sm cursor-pointer"
>
{config.notify_name} ({config.notification_type})
</Label>
</div>
))
) : (
<div className="text-sm text-muted-foreground">No notification channels available</div>
)}
</div>
{/* Selected Channels Display */}
{formData.notification_channels.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Selected Channels:</Label>
<div className="flex flex-wrap gap-2">
{formData.notification_channels.map((channelId) => {
const channel = alertConfigs.find(c => c.id === channelId);
return (
<div
key={channelId}
className="flex items-center gap-1 bg-secondary text-secondary-foreground px-2 py-1 rounded-md text-sm"
>
<span>{channel?.notify_name || channelId}</span>
<Button
type="button"
variant="ghost"
size="sm"
className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground"
onClick={() => removeNotificationChannel(channelId)}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})}
</div>
</div>
)}
</div>
{/* Server Set Threshold Selection */}
<div className="space-y-2">
<Label htmlFor="thresholdId">Server Set Threshold</Label>
<Select
value={formData.threshold_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, threshold_id: value }))}
disabled={loadingThresholds}
>
<SelectTrigger>
<SelectValue placeholder={loadingThresholds ? "Loading thresholds..." : "Select server threshold"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No threshold (use default)</SelectItem>
{thresholds.map((threshold) => (
<SelectItem key={threshold.id} value={threshold.id}>
{threshold.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Editable Threshold Details */}
{selectedThreshold && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold (%)</Label>
<Input
type="number"
min="0"
max="100"
value={thresholdFormData.cpu_threshold}
onChange={(e) => setThresholdFormData(prev => ({
...prev,
cpu_threshold: parseInt(e.target.value) || 0
}))}
className="mt-1"
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold (%)</Label>
<Input
type="number"
min="0"
max="100"
value={thresholdFormData.ram_threshold}
onChange={(e) => setThresholdFormData(prev => ({
...prev,
ram_threshold: parseInt(e.target.value) || 0
}))}
className="mt-1"
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold (%)</Label>
<Input
type="number"
min="0"
max="100"
value={thresholdFormData.disk_threshold}
onChange={(e) => setThresholdFormData(prev => ({
...prev,
disk_threshold: parseInt(e.target.value) || 0
}))}
className="mt-1"
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Network Threshold (%)</Label>
<Input
type="number"
min="0"
max="100"
value={thresholdFormData.network_threshold}
onChange={(e) => setThresholdFormData(prev => ({
...prev,
network_threshold: parseInt(e.target.value) || 0
}))}
className="mt-1"
/>
</div>
</div>
</CardContent>
</Card>
)}
{/* Server Template Selection */}
<div className="space-y-2">
<Label htmlFor="templateId">Server Template</Label>
<Select
value={formData.template_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, template_id: value }))}
disabled={loadingTemplates}
>
<SelectTrigger>
<SelectValue placeholder={loadingTemplates ? "Loading templates..." : "Select server template"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No template (use default)</SelectItem>
{templates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Template Details */}
{selectedTemplate && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-base">Template Details: {selectedTemplate.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 text-sm">
<div>
<Label className="text-xs font-medium text-muted-foreground">RAM Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.ram_message || "No RAM message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.cpu_message || "No CPU message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.disk_message || "No disk message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.network_message || "No network message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || "No up message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || "No down message defined"}
</p>
</div>
</div>
</CardContent>
</Card>
)}
</CardContent>
</Card>
)}
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
"Update Server"
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};
export default EditServerDialog;
@@ -0,0 +1,125 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Copy, Terminal } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface ManualInstallTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
};
serverId: string;
onDialogClose: () => void;
}
export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getManualInstallSteps = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return [
{
title: "Download the installation script",
command: `curl -L -o server-agent.sh "${scriptUrl}"`
},
{
title: "Make the script executable",
command: `chmod +x server-agent.sh`
},
{
title: "Run the installation with your configuration",
command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh`
}
];
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
Manual Installation Steps
</CardTitle>
<CardDescription>
Step-by-step installation process
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Server Name:</span> {formData.serverName}
</div>
<div>
<span className="font-medium">Agent ID:</span> {serverId}
</div>
<div>
<span className="font-medium">OS Type:</span> {formData.osType}
</div>
<div>
<span className="font-medium">Check Interval:</span> {formData.checkInterval}s
</div>
</div>
</div>
<div className="space-y-4">
{getManualInstallSteps().map((step, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center gap-2">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm font-medium">
{index + 1}
</span>
<span className="font-medium">{step.title}</span>
</div>
<div className="ml-8 relative">
<pre className="bg-muted p-3 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{step.command}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(step.command)}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
))}
</div>
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
<li>Ensure you have root/sudo access on the target server</li>
<li>Make sure curl is installed for downloading files</li>
<li>Internet connection required for downloading script</li>
</ul>
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
<p className="text-sm text-blue-800 dark:text-blue-200">
The agent will start automatically and appear in your dashboard within a few minutes.
</p>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</CardContent>
</Card>
);
};
@@ -0,0 +1,51 @@
import React from "react";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
interface OSOption {
value: string;
name: string;
logo: string; // now it's a path to image, e.g., /logos/ubuntu.svg
}
const osOptions: OSOption[] = [
{ value: "ubuntu", name: "Ubuntu", logo: "/upload/os/ubuntu.png" },
{ value: "debian", name: "Debian", logo: "/upload/os/debian.png" },
{ value: "centos", name: "CentOS", logo: "/upload/os/centos.png" },
{ value: "rhel", name: "Red Hat Enterprise Linux", logo: "/upload/os/rhel.png" },
{ value: "linux", name: "Linux (Generic)", logo: "/upload/os/linux.png" },
{ value: "windows", name: "Windows Server", logo: "/upload/os/windows.png" },
];
interface OSSelectorProps {
value: string;
onValueChange: (value: string) => void;
}
export const OSSelector: React.FC<OSSelectorProps> = ({ value, onValueChange }) => {
return (
<div className="grid gap-2">
{osOptions.map((os) => (
<button
key={os.value}
type="button"
onClick={() => onValueChange(os.value)}
className={cn(
"flex items-center justify-between w-full rounded-md border px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition",
value === os.value ? "bg-accent text-accent-foreground border-ring" : "border-input"
)}
>
<div className="flex items-center gap-3">
<img
src={os.logo}
alt={`${os.name} logo`}
className="w-6 h-6 object-contain"
/>
<span>{os.name}</span>
</div>
{value === os.value && <Check className="h-4 w-4" />}
</button>
))}
</div>
);
};
@@ -0,0 +1,27 @@
import { Monitor, Server, Smartphone, Laptop } from "lucide-react";
interface OSTypeIconProps {
osType: string;
className?: string;
}
export const OSTypeIcon = ({ osType, className = "h-4 w-4" }: OSTypeIconProps) => {
const getOSIcon = (os: string) => {
const osLower = os.toLowerCase();
if (osLower.includes('linux') || osLower.includes('ubuntu') || osLower.includes('centos') || osLower.includes('debian')) {
return <Server className={className} />;
} else if (osLower.includes('windows')) {
return <Monitor className={className} />;
} else if (osLower.includes('mac') || osLower.includes('darwin')) {
return <Laptop className={className} />;
} else if (osLower.includes('android') || osLower.includes('ios')) {
return <Smartphone className={className} />;
} else {
return <Server className={className} />;
}
};
return getOSIcon(osType);
};
@@ -0,0 +1,101 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface OneClickInstallTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getOneClickInstallCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return `curl -L -o server-agent.sh "${scriptUrl}"
chmod +x server-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
OS_TYPE="${formData.osType}" \\
CHECK_INTERVAL="${formData.checkInterval}" \\
RETRY_ATTEMPTS="${formData.retryAttempt}" \\
sudo -E bash ./server-agent.sh`;
};
const handleCopyCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
console.log('Copy button clicked'); // Debug log
const command = getOneClickInstallCommand();
console.log('Copying command:', command); // Debug log
await copyToClipboard(command);
};
return (
<Card className="border-green-500/20 bg-green-0/50 dark:bg-green-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
<Download className="h-5 w-5" />
One-Click Install (Recommended)
</CardTitle>
<CardDescription className="text-green-600 dark:text-green-300">
Copy and paste this single command to install the monitoring agent instantly
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-green-700 dark:text-green-400">Quick Install Command</Label>
<div className="relative">
<pre className="bg-black-50 dark:bg-green-100/950 border border-green-200 dark:border-green-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-green-800 dark:text-green-200">
<code>{getOneClickInstallCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-green-50 dark:bg-green-950/50 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900 text-green-700 dark:text-green-400"
onClick={handleCopyCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 p-3 rounded-md">
<p className="font-medium mb-1">Simply run this command on your server:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>SSH into your target server</li>
<li>Paste and run the command above</li>
<li>The agent will be installed and started automatically</li>
</ol>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</CardContent>
</Card>
);
};

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