Compare commits

...
Author SHA1 Message Date
gorkem-bwl dbccce445b fix(client): allow configurable Vite allowed hosts
Vite >=4.3 rejects requests with a Host header not in the allowlist,
which broke status-page submissions and dev-server access for users
serving Checkmate behind a reverse proxy after upgrading.

Adds VITE_ALLOWED_HOSTS env var (comma-separated list). When unset,
falls back to allowing all hosts so existing self-hosted deployments
keep working without changes.

Closes #3397
Closes #2951
2026-04-23 23:31:05 -04:00
Alexander Holliday 6ed0818bad Merge pull request #3566 from bluewave-labs/fix/build
fix: build
2026-04-23 11:07:39 -07:00
Alex Holliday f2e3640e28 add helper function to extract strings 2026-04-23 11:07:01 -07:00
Alex Holliday 8bfffd540d remove esbuild 2026-04-23 11:06:51 -07:00
Alex Holliday 559288fceb bump version 2026-04-23 10:14:05 -07:00
Alexander Holliday 8886566a8f Merge pull request #3564 from bluewave-labs/fix/resolve-incident-comment
fix comment type
2026-04-23 10:13:17 -07:00
Alex Holliday 28cb2aeacd fix comment type 2026-04-23 10:01:46 -07:00
Alex Holliday cbd82e29af bump version 2026-04-23 09:49:34 -07:00
Alexander Holliday 50876e57b9 Merge pull request #3520 from ZeinebMegaadi/docs/monitor-lifecycle-explained
docs: added monitor lifecycle explanation to README
2026-04-23 09:45:32 -07:00
Alexander Holliday b70916de20 Merge pull request #3561 from bluewave-labs/fix/line-endings
normalize line endings
2026-04-23 09:40:56 -07:00
Alex Holliday 3202a4276d normalize line endings 2026-04-23 09:30:43 -07:00
Gorkem Cetin 7a8caec159 Change demo link for Checkmate
Updated the demo link for Checkmate in the README.
2026-04-22 22:53:25 -04:00
Alexander Holliday 7e7f1bb6ca Merge pull request #3557 from bluewave-labs/fix/hide-register-tip-on-login
fix: hide register tip on login and redirect to setup when no super admin
2026-04-20 11:50:16 -07:00
Alex Holliday 5f12459bfd explicit import 2026-04-20 11:48:48 -07:00
Alex Holliday 27e866ba08 use lazy get 2026-04-20 11:47:43 -07:00
Alex Holliday f7a2b2af54 Merge branch 'develop' into fix/hide-register-tip-on-login 2026-04-20 11:28:25 -07:00
Alexander Holliday 3f35176651 Merge pull request #3558 from bluewave-labs/chore/remove-timescale-db
chore: remove timescale db
2026-04-20 11:28:07 -07:00
Alex Holliday 62c7e20726 purge pg types 2026-04-20 11:06:56 -07:00
Alex Holliday ff08645087 remove pool references, update docs 2026-04-20 11:04:43 -07:00
Alex Holliday 3e947ca2df remove bullmq, pg, bump dependencies 2026-04-20 11:03:13 -07:00
Alex Holliday cc32206547 purge timescaledb 2026-04-20 11:00:49 -07:00
Alexander Holliday 984a611d19 Merge pull request #3534 from egeoztass/feat/twilio-sms-notification
feat: add Twilio SMS notification channel
2026-04-20 10:54:12 -07:00
gorkem-bwl 39011e2e33 style: apply prettier formatting 2026-04-20 13:53:50 -04:00
gorkem-bwl 4998fe4c94 fix: hide register tip on login and redirect to setup when no super admin (#3556)
## Changes
- Removed the "Register here" link from the login page — it misled users into thinking public registration was open.
- Added redirect from /login to /register when no super admin exists (initial setup).
- Added an info alert on the register page clarifying the page is only for initial setup; additional users are invited from team settings. Hidden for the invite-token flow.
- Extracted shared useSuperAdminRedirect hook to dedupe identical fetch/redirect logic across Login and Register.

## Benefits
- Clearer UX: login page no longer suggests self-registration is available post-setup.
- Correct routing on fresh installs: users land on the registration page instead of a login page they can't use.
2026-04-20 13:50:36 -04:00
Gorkem Cetin 247308b636 Clarify demo access information in README
Separated demo credentials from the main demo description for clarity.
2026-04-20 12:38:02 -04:00
Alexander Holliday 33110cf6c8 Merge pull request #3545 from ZeinebMegaadi/fix/node-version-docs
docs: update Node.js version requirement to >=20 in contributing guide
2026-04-20 09:35:04 -07:00
Alexander Holliday 15c3fcb9b9 Merge pull request #3549 from SkyKingPX/develop
Add build_images.bat for Docker image builds
2026-04-20 09:34:24 -07:00
Alexander Holliday d5295f4f6c Merge pull request #3552 from Smolcup/docs/fix-code-of-conduct-repo-name-v2
docs: update repo name bluewave-uptime to checkmate in CODE_OF_CONDUCT
2026-04-20 09:33:30 -07:00
Alexander Holliday 886e04710f Merge pull request #3553 from Smolcup/docs/add-i18n-contribution-guide-v2
docs: update i18n contribution guide
2026-04-20 09:33:07 -07:00
Alexander Holliday d0e79ea99c Merge pull request #3555 from ZeinebMegaadi/fix/docs-link
fix: correct docs link URL (404 issue)
2026-04-20 09:30:34 -07:00
ZeinebMegaadi a0568a44a6 fix: correct docs link URL (404 issue) 2026-04-20 10:05:12 +01:00
Smolcup 4435a66255 docs: add i18n contribution guide 2026-04-19 21:21:04 +02:00
Smolcup dbc592b86b docs: update repo name bluewave-uptime to checkmate in CODE_OF_CONDUCT
Updated the Code of Conduct to reflect the new project name and ensure a welcoming environment for contributors.
2026-04-19 20:28:53 +02:00
SkyKing_PX 47a78edba8 Update Dockerfile paths for services in build_images.bat 2026-04-19 14:12:41 +02:00
SkyKing_PX f4fd836137 Fix paths in docker/coolify/build_images.sh 2026-04-19 14:12:00 +02:00
SkyKing_PX 6c6823cf76 Add build_images.bat for docker/coolify 2026-04-19 14:10:23 +02:00
SkyKing_PX 59187cc114 Add build_images.bat for docker/dist-mono 2026-04-19 14:07:32 +02:00
SkyKing_PX 0ad9ef15af Add build_images.bat for docker/dist 2026-04-19 14:06:14 +02:00
SkyKing_PX e23a185eaf Fix paths in docker/unified/build_image.sh 2026-04-19 14:04:37 +02:00
SkyKing_PX 36cbe4096d Add build_images.bat for docker/unified 2026-04-19 14:03:35 +02:00
SkyKing_PX ab692f8033 Add windows buildscript in docker/staging 2026-04-19 14:01:44 +02:00
SkyKing_PX 15542154f4 Fix paths 2026-04-19 14:00:33 +02:00
SkyKing_PX 7eb7fa1629 Add build_images.bat for Docker image production builds 2026-04-19 13:50:33 +02:00
ZeinebMegaadi 7906fce77f docs: update Node.js version requirement to >=20 in contributing guide 2026-04-17 21:17:40 +01:00
egeoztassandClaude Opus 4.6 c57614e8df fix: add accountSid and twilioPhoneNumber to notification repositories
Updated both MongoDB and TimescaleDB repositories to map the new
Twilio-specific fields (accountSid, twilioPhoneNumber) in create,
update, and toEntity methods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:14:16 +03:00
egeoztassandClaude Opus 4.6 d5d6797f91 fix: use dedicated Twilio fields and add service switch case tests
- Added `accountSid` and `twilioPhoneNumber` fields to Notification
  type, MongoDB model, and frontend type instead of reusing
  `address` and `homeserverUrl`
- Updated provider, validation, form hook, and form UI to use new fields
- Added Pushover and Twilio to notificationsService routing tests
  (both handleNotifications and sendTestNotification)
- All 59 tests passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:11:11 +03:00
Alexander Holliday 77545e9cf7 Merge pull request #3544 from bluewave-labs/fix/unified-docker-compose
fix/unified docker compose
2026-04-17 12:26:31 -07:00
Alex Holliday 29ea54419c add staging 2026-04-17 19:20:31 +00:00
Alexander Holliday 9042cb917a Merge pull request #3535 from Smolcup/docs/fix-discussions-link-readme-es
docs: fix discussions link in Spanish README
2026-04-17 12:12:38 -07:00
Alexander Holliday a65e1071a5 Merge pull request #3541 from Smolcup/docs/fix-typo-pullrequests
docs: fix typo 'big' to 'bug' in PULLREQUESTS.md
2026-04-17 11:28:32 -07:00
Alex Holliday 4493cca871 Merge branch 'fix/unified-docker-compose' of github.com:bluewave-labs/checkmate into fix/unified-docker-compose 2026-04-17 11:26:20 -07:00
Alex Holliday 999dfb13b9 update workflows 2026-04-17 11:25:53 -07:00
Alex Holliday 97f4008f4c remove explicit ports 2026-04-17 18:24:01 +00:00
Alex Holliday 6152144807 add staging, comment out for now 2026-04-17 11:18:03 -07:00
Alex Holliday b376fe9ab0 comment out staging 2026-04-17 11:07:25 -07:00
Alex Holliday aa3e097e16 initial commit 2026-04-17 10:54:39 -07:00
Alexander Holliday d77aeeb46c Merge pull request #3542 from bluewave-labs/fix/docker-compose
fix: update production docker compose/nginx conf
2026-04-17 10:37:58 -07:00
Alex Holliday 82b7ab83dd update production docker compose/nginx conf 2026-04-17 17:34:23 +00:00
Smolcup 88bc4e080d docs: fix typo 'big' to 'bug' in PULLREQUESTS.md 2026-04-17 19:27:57 +02:00
Alexander Holliday fa6aef1e86 Merge pull request #3518 from ZeinebMegaadi/fix/showing-down-for-down-monitors
Fix: added "down" display on down monitors instead of "Active for:..."
2026-04-17 10:17:34 -07:00
Gorkem Cetin 57ed0e1e7d Enhance README with PikaPods run button
Added a run button for PikaPods and improved project description.
2026-04-17 11:46:37 -04:00
ZeinebMegaadi 26077151ef fix: added maintenance state 2026-04-16 20:54:09 +01:00
Smolcup 55b05a2100 docs: fix discussions link in Spanish README 2026-04-16 21:43:03 +02:00
egeoztassandClaude Opus 4.6 9210259511 feat: add Twilio SMS notification channel
Adds Twilio SMS as a new notification channel, allowing users to
receive SMS alerts via the Twilio API.

Backend: provider using HTTP Basic Auth, type registration, validation,
service wiring. Uses existing fields: address (Account SID),
accessToken (Auth Token), homeserverUrl (From number), phone (To number).
Frontend: form UI with 4 fields, validation, i18n keys.
Tests: 18 provider tests + updated service test constructor.

Closes #3095

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:46:40 +03:00
Alexander Holliday e9a0914fad Merge pull request #3530 from egeoztass/feat/pushover-notification
feat: add Pushover notification channel
2026-04-16 11:21:43 -07:00
Alexander Holliday f392bee287 Merge pull request #3533 from bluewave-labs/fix/monitor-status-atomicity
fix: monitor status atomicity
2026-04-16 11:14:57 -07:00
egeoztassandClaude Opus 4.6 4281e87642 fix: add pushoverProvider to NotificationsService test constructor
The existing test was missing the new pushoverProvider argument,
causing settingsService to be passed in the wrong position.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:14:47 +03:00
Alex Holliday d40b766c83 timescaleDB implementation 2026-04-16 11:12:06 -07:00
Alexander Holliday d139db7697 Merge pull request #3512 from SkyKingPX/feat/improved-status-bar-messages
Improve status bar messages on public status pages
2026-04-16 11:02:07 -07:00
Alex Holliday 7547d1db2e update tests 2026-04-16 10:59:43 -07:00
Alex Holliday 24d7ead1d1 use a projected window instead of read/write 2026-04-16 10:59:30 -07:00
SkyKingPX e60dbb9f74 Add .gitattributes file so LF endings are preserved 2026-04-16 19:57:21 +02:00
Alexander Holliday f12a96ce58 Merge pull request #3532 from mayssagl/fix/email-templates-missing-from-dist
fix: copy email templates to dist during build
2026-04-16 10:56:55 -07:00
SkyKingPX 5920b92869 Run npm format 2026-04-16 19:55:17 +02:00
Alex Holliday 8f55d966f9 rename methods, remove unused helpers 2026-04-16 10:50:30 -07:00
Alex Holliday c7ef412d7a atomic update 2026-04-16 10:41:00 -07:00
mayssagl 14f76ec10b fix: copy email templates to dist during build 2026-04-16 18:35:17 +01:00
ZeinebMegaadi d532d13371 fix(lifecycle updated) 2026-04-16 18:33:44 +01:00
ZeinebMegaadi 58532d1ff6 fix(monitors): handle initializing and breached states in stat box 2026-04-16 18:09:05 +01:00
Alexander Holliday b5fe314dde Merge pull request #3531 from mayssagl/fix-claude.md
docs: improve CLAUDE.md with architecture flow and repository pattern…
2026-04-16 10:01:33 -07:00
SkyKingPX 0d163e7100 Change Breached to exceeded thresholds 2026-04-16 18:56:06 +02:00
mayssagl 5dda0b51c3 docs: improve CLAUDE.md with architecture flow and repository pattern explanation 2026-04-16 17:42:11 +01:00
Alexander Holliday 273aa7d01a Merge pull request #3529 from SkyKingPX/feat/docker-images-script-windows
Add windows batch script for creating docker images automatically
2026-04-16 09:41:15 -07:00
egeoztassandClaude Opus 4.6 cc1b6df8f2 style: run prettier on notification create page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:55:30 +03:00
egeoztassandClaude Opus 4.6 97353bc7d2 feat: add Pushover notification channel
Adds Pushover as a new notification channel, allowing users to receive
push notifications on their devices via the Pushover API.

Backend: provider, type registration, validation, service wiring
Frontend: form UI with app token and user key fields, validation, i18n
Tests: full unit test suite (15 tests, 100% coverage on provider)

Closes #3445

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:25:24 +03:00
SkyKingPX 10a703d657 Update CONTRIBUTING.md 2026-04-16 13:49:58 +02:00
SkyKingPX b3077f6239 Merge branch 'feat/docker-images-script-windows' of https://github.com/SkyKingPX/Checkmate into feat/docker-images-script-windows 2026-04-16 13:46:33 +02:00
SkyKingPX 80ae5a1977 Add windows docker image build script 2026-04-16 13:46:16 +02:00
SkyKingPX 8579708f05 Add script 2026-04-16 13:41:30 +02:00
SkyKing_PX 675ec40836 Merge branch 'bluewave-labs:develop' into feat/improved-status-bar-messages 2026-04-16 13:40:10 +02:00
SkyKingPX 7b164e4df5 Make requested changes 2026-04-16 13:37:54 +02:00
Alexander Holliday 2be1b48995 Merge pull request #3527 from bluewave-labs/fix/demo-user
fix: demo user
2026-04-15 14:02:22 -07:00
Alex Holliday be3b7c2e86 add demo role to list of roles 2026-04-15 13:54:05 -07:00
Alex Holliday 69bec42a66 restrict editing user routes to regular users 2026-04-15 13:53:55 -07:00
Alexander Holliday 12b879ced5 Merge pull request #3526 from bluewave-labs/fix/notification-deletion
fix: remove notifications from monitors before deleting notifications
2026-04-15 13:31:42 -07:00
Alex Holliday 9e24cc655c remove notifications from monitors before deleting notifications 2026-04-15 13:21:14 -07:00
Alexander Holliday 95db80db5d Merge pull request #3525 from bluewave-labs/feat/initializaiton-tests
feat: initializaiton tests
2026-04-15 11:19:07 -07:00
Alexander Holliday ab7f2c42f5 Merge pull request #3524 from bluewave-labs/fix/streak-time
fix: tighten streak time coniditional
2026-04-15 11:09:33 -07:00
Alex Holliday be462811ac test initializing state 2026-04-15 11:08:40 -07:00
Alex Holliday 564b356115 tighten streak time coniditional 2026-04-15 11:06:30 -07:00
Alexander Holliday bcacd42162 Merge pull request #3523 from bluewave-labs/fix/initialization-state
fix: monitor init
2026-04-15 11:01:15 -07:00
Alex Holliday 6f7fba5ae5 fixes a bug where state was not correctly calcluated for monitors in initializing state 2026-04-15 10:54:43 -07:00
Alexander Holliday 61d753b119 Merge pull request #3522 from bluewave-labs/feat/additional-heartbeat-tests
feat: add additional heartbeat tests
2026-04-15 10:33:44 -07:00
ZeinebMegaadi b482e871a3 updated fix for same issue 2026-04-15 16:12:10 +01:00
ZeinebMegaadi bca602f4b0 docs: add monitor lifecycle explanation to README 2026-04-15 15:13:35 +01:00
ZeinebMegaadi f6222014b7 Fix: correct Active for display on down monitors 2026-04-15 14:51:12 +01:00
SkyKing_PX 360e8ad2a0 Merge branch 'bluewave-labs:develop' into feat/improved-status-bar-messages 2026-04-14 20:47:59 +02:00
Alex Holliday e8bcfdd21d add additional heartbeat tests 2026-04-14 11:31:09 -07:00
Alexander Holliday fd8dce8abe Merge pull request #3510 from bluewave-labs/feat/monitor-status-refactor
feat/monitor status refactor
2026-04-14 10:59:44 -07:00
Alex Holliday b9c14c76c9 add tests for new helper functions 2026-04-14 10:46:26 -07:00
Alex Holliday bf60c52692 simplify hardware changes 2026-04-14 10:41:55 -07:00
SkyKingPX a8339c0169 Improve status bar messages on public status pages 2026-04-14 19:32:05 +02:00
Alex Holliday d74f1bb6a8 add private helper methods to updateMonitorStats 2026-04-14 10:28:43 -07:00
Alexander Holliday 9bb7224fc4 Merge pull request #3505 from bluewave-labs/fix/notification-fix-down
fix: notification down not sent
2026-04-14 09:43:05 -07:00
Alex Holliday 403290db32 Merge branch 'develop' into fix/notification-fix-down 2026-04-14 09:41:16 -07:00
Alexander Holliday 98ae6b1105 Merge pull request #3508 from bluewave-labs/fix/monitor-status-tests
fix: additional status service tests
2026-04-14 09:40:58 -07:00
Alex Holliday 2014825618 additional tests 2026-04-14 09:40:14 -07:00
Alexander Holliday 4a61bc88d3 Merge pull request #3507 from bluewave-labs/fix/monitor-status-tests
fix: add test for monitor status
2026-04-14 09:37:27 -07:00
Alex Holliday fa4c0632ae add test for monitor status 2026-04-14 09:35:58 -07:00
mannilakash ece16158ef fix: notification down not sent 2026-04-14 12:07:51 -04:00
Alexander Holliday bd27721685 Merge pull request #3502 from bluewave-labs/feat/test-workflow
feat: add check-tests workflow
2026-04-13 13:11:23 -07:00
Alex Holliday 7c8549d092 fix messages 2026-04-13 13:08:33 -07:00
Alex Holliday 1e8c9d0187 stub out poeditor-sync 2026-04-13 13:07:21 -07:00
Alexander Holliday faa59ab694 Merge pull request #3501 from bluewave-labs/fix/notification-timeouts
fix: notification timeouts
2026-04-13 13:03:37 -07:00
Alex Holliday de4de0f5db add check-tests workflow 2026-04-13 13:03:22 -07:00
Alex Holliday 48c1e326c8 use relative paths for testing 2026-04-13 12:59:49 -07:00
Alex Holliday 22f54391d0 use abstract class 2026-04-13 12:55:20 -07:00
Alex Holliday 0bb3320010 add abstract class with default timeout and helper method 2026-04-13 12:54:51 -07:00
Alexander Holliday 7998bae87d Merge pull request #3500 from bluewave-labs/fix/invite-validation
fix: enforce correct role types
2026-04-13 12:37:33 -07:00
Alex Holliday ec929aef45 enforce correct role types 2026-04-13 12:26:14 -07:00
Alexander Holliday 77e72bc3bf Merge pull request #3499 from bluewave-labs/fix/update-running-stats
fix: remove in memory modification of monitor stats
2026-04-13 12:18:31 -07:00
Alex Holliday e1b00ce735 dedupe monitor stats, run migrations before index creation 2026-04-13 11:58:24 -07:00
Alex Holliday 0fcb38f7e6 Merge branch 'develop' into fix/update-running-stats 2026-04-13 11:48:18 -07:00
Alex Holliday b3930b31f2 remove in memory modification of monitor stats 2026-04-13 11:39:29 -07:00
Alexander Holliday 6222635eef Merge pull request #3498 from bluewave-labs/fix/stats-init
fix: initialize timeOfLastFailure
2026-04-13 11:01:59 -07:00
Alex Holliday dc7688bc13 initialize timeOfLastFailure 2026-04-13 10:58:58 -07:00
Gorkem Cetin c287cf16d1 Update pull_request_template.md 2026-04-13 13:38:48 -04:00
Alexander Holliday ff114b3ab2 Merge pull request #3494 from bluewave-labs/feat/integration-tests
feat: integration tests
2026-04-11 15:03:22 -07:00
Alex Holliday 233966b73d add heartbeat tests 2026-04-11 14:45:36 -07:00
Alex Holliday 45d4779b45 fix race condition in heartbeat job 2026-04-11 13:57:57 -07:00
Alexander Holliday d608b5ea24 Merge pull request #3492 from bluewave-labs/feat/service-tests
feat/service tests
2026-04-10 12:43:54 -07:00
Alex Holliday 1ad750e2ef fix ternary condiiton 2026-04-10 12:41:46 -07:00
Alex Holliday 4d1af5e2b4 fix TS errors, add missing tests 2026-04-10 12:33:08 -07:00
Alex Holliday b515d688f4 add geo check service tests 2026-04-10 11:43:02 -07:00
Alex Holliday ff92b307bb add diagnosticService tests 2026-04-10 11:39:26 -07:00
Alex Holliday e4ffe0e267 inviteSerice tests 2026-04-10 11:30:31 -07:00
Alex Holliday 2b121355c8 status page service tests 2026-04-10 11:25:54 -07:00
Alex Holliday e7db938baa import monitors bug fix 2026-04-10 11:15:28 -07:00
Alex Holliday 1b746d0f0a remvoe dead code from monitor service 2026-04-10 11:13:12 -07:00
Alex Holliday 6886fc75de monitorService tests 2026-04-10 11:07:48 -07:00
Alex Holliday 5c5f84a5b2 user service tests 2026-04-10 10:59:48 -07:00
Alex Holliday a12154523f settingsService tests 2026-04-10 10:50:37 -07:00
Alex Holliday 6c8adf8594 add tests for maintenancewindowsercie 2026-04-10 10:39:04 -07:00
Alex Holliday a0f73e4e8d add notificaiton provider tests 2026-04-09 11:05:21 -07:00
Alex Holliday 63efef65c7 add provider tests 2026-04-09 10:36:59 -07:00
Alex Holliday dca09daf35 add tests for globalPingService 2026-04-09 10:03:23 -07:00
Alex Holliday db833b441a add tests for emailService 2026-04-09 09:49:12 -07:00
Alex Holliday bb55f3b3eb add tests for notificaitonMessageBuilder 2026-04-09 09:41:17 -07:00
Alex Holliday 88e0660762 add tests for buffer service 2026-04-09 09:36:51 -07:00
Alex Holliday a8c172c77f add tests for network service 2026-04-09 09:27:38 -07:00
Alex Holliday b355da4aed Merge branch 'develop' into feat/server-testing 2026-04-09 09:15:55 -07:00
Alexander Holliday 77a4bf9c36 Merge pull request #3479 from ad2ien/fix/matrix-notif
Fix matrix notifications
2026-04-09 09:15:15 -07:00
Adrien Touzouli 6a0f4372c6 fix(notifications): fix matrix notifications
Respect Matrix spec, factorize api call and improve error log
2026-04-09 06:08:05 +02:00
Alex Holliday db378120be add queue tests 2026-04-08 12:03:26 -07:00
Alex Holliday ba5368f942 add notificationsService tests 2026-04-08 11:32:08 -07:00
Alex Holliday 7329306ccf add incident serivce tests 2026-04-08 10:43:47 -07:00
Alex Holliday 365987c287 relocate tests, add statusService tests 2026-04-08 10:31:24 -07:00
Gorkem Cetin 09d6147f75 Remove Translations section from README
Removed the Translations section from the README.
2026-04-08 01:26:16 -04:00
Gorkem Cetin 86ab1f2146 Add Coolify and Cloudzy 2026-04-07 18:34:28 -04:00
Alexander Holliday 934f85f505 Merge pull request #3460 from bluewave-labs/fix/server-test-suite-failures
fix: align server test suite with current source implementations
2026-04-07 11:11:51 -07:00
Alex Holliday 96beda5c47 update mock logger and tests 2026-04-07 11:10:24 -07:00
Alexander Holliday 504d6157e5 Merge pull request #3473 from bluewave-labs/fix/actions
fix: disable poeditor workflow
2026-04-07 09:58:41 -07:00
Alex Holliday 628bca3e29 disable poeditor workflow 2026-04-07 09:56:11 -07:00
Alexander Holliday 2c76484bad Merge pull request #3471 from bluewave-labs/feat/zh-tw
feat: add remaining zh-tw strings
2026-04-07 09:53:58 -07:00
Alex Holliday 22498756cd add remaining zh-tw strings 2026-04-07 09:46:07 -07:00
Alexander Holliday 7c61ca490c Merge pull request #3461 from bluewave-labs/fix/i18n-locale-migration
fix: restructure and translate all i18n locale files
2026-04-03 20:46:13 -07:00
gorkem-bwl 96f9360280 style: format migration script with prettier 2026-04-03 23:43:34 -04:00
gorkem-bwl eb4a92c84f fix: restructure and translate all i18n locale files
All 15 non-English locale files had a structural mismatch with en.json:
- en.json used nested keys (common.buttons.cancel)
- Other locales used flat keys (cancel, DeleteAccountButton)
- Result: 0.3% effective translation coverage at runtime

## Changes

Migration script (client/scripts/migrate-locales.ts):
- Restructures all locale files to match en.json's nested key structure
- Recovers existing translations via safe key-matching strategies
- Backs up originals to locales/backup/

Translation of all 15 languages:
- ar (94.6%), cs (91.6%), de (89.4%), es (92.6%), fi (94.2%)
- fr (86.7%), ja (93.3%), pt-BR (91.0%), ru (93.0%), th (93.4%)
- tr (91.9%), uk (94.3%), vi (92.5%), zh-CN (94.0%), zh-TW (94.2%)

Keys intentionally left in English are technical terms (HTTP, CPU,
Docker, gRPC, etc.), placeholder examples, and brand names.

en.json was NOT modified.
2026-04-03 23:35:14 -04:00
gorkem-bwl c80720c8c9 fix: align server test suite with current source implementations
## Changes
- teamsProvider.test.ts: fix got mock wiring (use top-level mockPost
  with dynamic import so mock intercepts correctly), fix getTestMessage
  assertion, fix incident URL path
- superSimpleQueueHelper.test.ts: change default import to named import,
  update constructor from object to 16 positional params, rename
  getMonitorJob -> getHeartbeatJob, add all missing dependencies
- monitorService.test.ts: remove stale constructor params (db,
  stringService, papaparse, errorService), add missing params
  (geoChecksRepository, incidentsRepository), fix method names
  (findLatestChecksByMonitorIds -> findLatestByMonitorIds,
  findDateRangeChecksByMonitor -> findByDateRangeAndMonitorId),
  update assertions from checks -> recentChecks
- jest.config.ts: consolidate @/utils path mapping to resolve .ts files
- Extract shared createMockLogger to test/helpers/

Closes #3457
2026-04-03 21:56:14 -04:00
Alexander Holliday 42e82af9bb Merge pull request #3458 from bluewave-labs/feat/scheduler/1.9.1
feat: bump scheduler, consume scheduler events
2026-04-03 15:38:12 -07:00
Alex 7636d2d090 error -> warn on job fail, register listeners before starting scheduler 2026-04-03 15:36:37 -07:00
Alex Holliday 72728748f8 bump scheduler, consume scheduler events 2026-04-03 13:21:16 -07:00
Alexander Holliday 4222d39a8b Merge pull request #3456 from bluewave-labs/fix/job-queue
fix geocheck update logic
2026-04-01 11:04:50 -07:00
Alex Holliday fb69e29adf fix geocheck update logic 2026-04-01 11:00:17 -07:00
Alexander Holliday 0d90083e63 Merge pull request #3455 from mayssagl/develop
Fix outdated links and add Prerequisites section
2026-04-01 10:10:48 -07:00
Alexander Holliday 8ab049aa6c Merge pull request #3447 from bluewave-labs/feat/dlq
Feat(DLQ): Add DLQ data layer -> types, model, and repository
2026-03-27 12:35:31 -07:00
Alexander Holliday 69b6955ced Merge pull request #3439 from LeC-D/fix/telegram-notification-type
feat(notifications): add Telegram notification channel (#2753)
2026-03-27 12:04:01 -07:00
LeC-D 04dfff4ebd fix(notifications): remove redundant defaultValue prop and fix incident URL in telegram provider
- Remove redundant 'defaultValue' prop on Telegram chat ID Controller in create/index.tsx;
  the form already initialises via 'defaultValues: defaults' at the useForm level, making
  the per-Controller prop unnecessary and inconsistent with the reviewer's expectation.
- Fix 'View Incident' URL in telegram.ts: was pointing to the infrastructure details page
  (/infrastructure/:id) instead of the incidents page (/incidents/:id).
2026-03-27 14:19:27 -04:00
Aryaman Sharma 15b0deb0bd Merge branch 'develop' into feat/dlq 2026-03-27 23:24:45 +05:30
Br0wnHammer a895912b99 refactor(dlq): inline DLQItemDocumentBase into DLQItemDocument 2026-03-27 23:23:01 +05:30
Alexander Holliday 22d2626a93 Merge pull request #3441 from bluewave-labs/feat/timescaledb
feat: timescaledb
2026-03-27 10:40:17 -07:00
Br0wnHammer 214fc4899c feat(dlq): add MongoDLQRepository with retry polling and grouped count queries 2026-03-27 20:08:18 +05:30
Br0wnHammer e6088fe06f feat(dlq): add IDLQRepository interface for dead letter queue data access 2026-03-27 20:07:00 +05:30
Br0wnHammer 60b1d95b74 feat(dlq): add DLQItem Mongoose model with indexes for retry polling and cleanup 2026-03-27 20:06:12 +05:30
Br0wnHammer 94ca9896db feat(dlq): add DLQItem type definitions for dead letter queue 2026-03-27 20:05:03 +05:30
LeC-D 13a7ae82e3 fix(notifications): remove type casts with exhaustive type narrowing in useNotificationForm
Replace the loose NotificationDefaults intermediate type and 'as NotificationFormData'
cast with an exhaustive buildDefaults() helper that narrows each branch of the
discriminated union explicitly. TypeScript now infers the return type correctly from
the discriminated union without any casts, satisfying strong typing for all channels
(email, slack, discord, webhook, pager_duty, teams, matrix, telegram).
2026-03-26 14:07:50 -04:00
Alex Holliday de3af64254 migrations, gitignore 2026-03-26 10:57:20 -07:00
Alex Holliday 317d531775 remove comments 2026-03-26 10:44:59 -07:00
Alex Holliday 2b2db2df6f set default all for incident validation 2026-03-26 10:33:19 -07:00
Alex Holliday c558c4b635 use ca tables 2026-03-26 10:30:15 -07:00
Alex Holliday be29cce788 db types 2026-03-26 09:47:10 -07:00
Alex Holliday e87879970a fix TS errors 2026-03-26 09:30:20 -07:00
Leandre Chamberland-Dozois 4c94a7c21e fix: resolve TypeScript build errors in notification form types 2026-03-25 19:13:37 -04:00
LeC-D 40968c370b refactor(notifications): hoist default values, remove else block in useNotificationForm
Per @ajhollid review: define defaults up-front with a NotificationDefaults type
and eliminate the trailing else branch entirely. The if/else-if blocks only
override when the type is matrix or telegram.
2026-03-25 18:06:26 -04:00
LeC-DandClaude Opus 4.6 a864bc0c05 fix: run format and fix build errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 16:52:48 -04:00
Alex Holliday 62e86434c2 checks repo, barrel export 2026-03-25 13:52:18 -07:00
Alex Holliday 07957d3a1f geo-checks repo 2026-03-25 13:52:10 -07:00
Alex Holliday bc088c3589 add incidents repo, rename old repo 2026-03-25 13:51:57 -07:00
Alex Holliday 5ea86d8e71 add inivtes repo 2026-03-25 13:51:37 -07:00
Alex Holliday 0944dba386 add maintenance-windows repo 2026-03-25 13:51:23 -07:00
Alex Holliday 86acc0c30a add monitor-stats repo 2026-03-25 13:51:12 -07:00
Alex Holliday dcdde845db monitors rep 2026-03-25 13:51:03 -07:00
Alex Holliday 3039fb9b69 nottificaitons repo 2026-03-25 13:50:46 -07:00
Alex Holliday 729600a86f recovery tokens repo 2026-03-25 13:50:33 -07:00
Alex Holliday 51853cb360 add settings repo 2026-03-25 13:50:22 -07:00
Alex Holliday f7db732d67 add status-pages repo 2026-03-25 13:50:12 -07:00
Alex Holliday 9e686a2319 add user repo 2026-03-25 13:49:56 -07:00
Alex Holliday da6f03ba59 add teams repo 2026-03-25 13:49:46 -07:00
Alex Holliday 276b11603e throw error on missing continent 2026-03-25 13:49:13 -07:00
Alex Holliday f09f0630dc update error handler 2026-03-25 13:48:50 -07:00
Alex Holliday 1defa52ae8 update config 2026-03-25 13:48:36 -07:00
Alex Holliday f43b7584a6 add PG dependencies 2026-03-25 13:47:56 -07:00
Alex Holliday ee74584699 Add method to interface 2026-03-25 13:47:36 -07:00
Alex Holliday 2422fcc83b update gitignore 2026-03-25 13:47:14 -07:00
Alex Holliday 0ecc2e21e5 Implemement TimescaleDB class 2026-03-25 13:47:01 -07:00
LeC-D fe242b1977 fix: replace nested ternaries and remove type casts per review
- useNotificationForm: replace nested ternary with if/else if/else block
- TelegramProvider: type error as unknown and access properties safely
  via instanceof checks instead of casting to HTTPError
2026-03-25 14:03:49 -04:00
LeC-D d1f1d31ea2 fix(i18n): revert en.json to upstream format, keep only telegram keys
The previous commit reformatted the entire en.json (spaces → kept as-is
but accumulated unrelated upstream changes), creating a 1.3k line diff.

Restore the file to match upstream/develop exactly and add only the
Telegram-specific i18n keys required by this PR.
2026-03-24 17:22:34 -04:00
Léo eacbb227c3 feat(notifications): add Telegram notification channel (#2753)
Add Telegram as a new notification type, allowing users to receive
monitor alerts via a Telegram bot.

Configuration requires:
- Bot token (obtained from @BotFather)
- Chat ID (user, group, or channel)

Changes:
- TelegramProvider: sends HTML-formatted messages via Telegram Bot API
- NotificationsService: route telegram type to TelegramProvider
- Notification model/types: add 'telegram' to NotificationChannel enum
- Server validation: telegram schema (accessToken + address/chat_id)
- Client Types/Validation: telegram schema with accessToken + address
- useNotificationForm: handle telegram defaults
- Notifications create page: render Telegram config UI
- en.json: add telegram i18n keys
2026-03-24 10:15:46 -04:00
Alexander Holliday c24b41ecd9 Merge pull request #3437 from bluewave-labs/fix/notificaiton-upper-limit
fix: add upper limit to notificationValidation
2026-03-23 10:16:05 -07:00
Alex 94cf4f17d1 add upper limit to notificationValidation 2026-03-23 10:13:43 -07:00
Alexander Holliday 0a6d876f4f Merge pull request #3376 from ashu130698/feat/bulk-edit-notifications
feat: add bulk edit monitor notifications endpoint
2026-03-23 10:11:12 -07:00
Mayssaaa gloulou ec129e3604 Fix outdated links and add Prerequisites section 2026-03-23 13:49:08 +01:00
ashu130698 833e2b63ad fix: resolve duplicate identifiers and missing interface method from merge 2026-03-21 01:24:52 +05:30
Alexander Holliday bc2bab87ba Merge pull request #3436 from bluewave-labs/fix/remaining-lint-errors
fix: resolve remaining ESlint errors
2026-03-20 12:42:54 -07:00
Alex Holliday ee5f7427e7 resolve remaining ESlint errors 2026-03-20 12:40:27 -07:00
Alexander Holliday d5968b841e Merge branch 'develop' into feat/bulk-edit-notifications 2026-03-20 12:28:57 -07:00
Alexander Holliday 26bac6d35a Merge pull request #3435 from bluewave-labs/fix/migrations
fix: migration lint errors
2026-03-20 12:24:08 -07:00
ashu130698 3e010f1c0d style: run prettier to fix formatting issues after upstream merge 2026-03-21 00:52:36 +05:30
Alex Holliday 1a504a3cd9 fix migraiton lint errors 2026-03-20 12:22:22 -07:00
Alex Holliday 120c5cd472 repo lint errors 2026-03-20 12:16:23 -07:00
Alexander Holliday cdf17bf600 Merge pull request #3433 from bluewave-labs/fix/controllers
fix/controllers
2026-03-20 11:55:59 -07:00
Alex Holliday 7144c20c5b check,auth 2026-03-20 11:52:53 -07:00
Alex Holliday 7456609e35 diagnostic 2026-03-20 11:51:54 -07:00
Alex Holliday 05c293d689 geo 2026-03-20 11:51:17 -07:00
Alex Holliday 80f3168837 invite, inident 2026-03-20 11:50:47 -07:00
Alex Holliday 332300dde4 log controller 2026-03-20 11:48:58 -07:00
Alex Holliday d4a8023a76 monitors 2026-03-20 11:47:22 -07:00
Alex Holliday c7e449d18c notifications 2026-03-20 11:45:04 -07:00
Alex Holliday 0495bef469 queue 2026-03-20 11:43:37 -07:00
Alex Holliday 6fb7ad63f6 settings 2026-03-20 11:42:40 -07:00
Alex Holliday 8322adbb26 statusPageController interface 2026-03-20 11:39:19 -07:00
Alexander Holliday 4470c87d42 Merge pull request #3431 from bluewave-labs/fix/infra-services
fix: infra services
2026-03-20 11:37:19 -07:00
Alexander Holliday 95bda35ec6 Merge pull request #3430 from bluewave-labs/fix/services
fix/services
2026-03-20 11:33:48 -07:00
Alex Holliday 928ab7551e fixes 2026-03-20 11:33:40 -07:00
Alex Holliday 465cea21bc use interface 2026-03-20 11:29:44 -07:00
Alex Holliday 404bd5d06b add interface to AdvancedMathcer, fix type 2026-03-20 11:28:53 -07:00
ashu130698 05814854df Merge branch 'upstream/develop' into feat/bulk-edit-notifications and resolve conflicts 2026-03-20 23:54:33 +05:30
Alex Holliday a4a3249b5b maintenacne window 2026-03-20 11:20:22 -07:00
ashu130698 1e6cf858c4 fix: address PR review feedback and add Zod validation constraints 2026-03-20 23:39:38 +05:30
Alex Holliday 533a8911cd fix errors in inviteService 2026-03-20 11:03:45 -07:00
Alex Holliday 2fb7516ca1 fix return type 2026-03-20 11:00:30 -07:00
Alex Holliday 6d2008e8d3 geo checks service 2026-03-20 10:59:34 -07:00
Alex Holliday 7332b15242 fix diagnosticSerivce lint error 2026-03-20 10:51:06 -07:00
Alexander Holliday d618a861e3 Merge pull request #3429 from bluewave-labs/fix/monitor-service-lint
fix/monitor service lint
2026-03-20 10:43:32 -07:00
Alex Holliday f69752ccb4 remove casts 2026-03-20 10:37:29 -07:00
Alex Holliday 210881a375 remove unused import 2026-03-20 10:17:24 -07:00
Alex Holliday d8ec8d7775 fix validation 2026-03-20 10:09:29 -07:00
Alex Holliday bfd3eee94d monitorService types 2026-03-20 09:58:49 -07:00
Alex Holliday bfbb4c390a Add type for getGeoChecksByMonitorId 2026-03-20 09:47:02 -07:00
Alexander Holliday 1285a641e8 Merge pull request #3428 from bluewave-labs/fix/status-page-swr-config
fix: modifying refreshInterval=10s for statusPage api
2026-03-20 09:38:46 -07:00
mannilakash 78566c0226 fix: modifying refreshInterval=10s for statusPage api 2026-03-19 11:34:26 -04:00
Alexander Holliday f5ba387261 Merge pull request #3427 from bluewave-labs/fix/swagger-post-monitor-schema
fix: updating swagger for createMonitor and /monitor/teams
2026-03-18 14:40:58 -07:00
Alexander Holliday 48b415a67c Merge pull request #3426 from bluewave-labs/fix/swagger-put-monitor
fix: updating swagger - /PUT to /PATCH monitor/{monitorId}
2026-03-18 14:40:34 -07:00
Alexander Holliday b53f0e56ea Merge pull request #3425 from bluewave-labs/feat/status-page-infra-section-fe
feat: status-page infra section FE
2026-03-18 14:40:01 -07:00
Alexander Holliday 07f828a01a Merge pull request #3424 from bluewave-labs/feat/status-page-infra-section-be
feat: status-page infrastructure section BE
2026-03-18 14:38:57 -07:00
mannilakash b09203664d fix: updating swagger for createMonitor and /monitor/teams 2026-03-18 17:30:19 -04:00
mannilakash 5a12105c3d fix: statusPageMonitor removal of infrastructureMonitors[] 2026-03-18 17:02:17 -04:00
mannilakash 22f79144d8 fix: hardcoded statusPage value fix 2026-03-18 16:25:07 -04:00
mannilakash f749ed46a6 fix: updating swagger - schema changes monitor/{monitorId} 2026-03-18 16:11:36 -04:00
mannilakash 5c23ebe315 fix: updating swagger - /PUT to /PATCH monitor/{monitorId} 2026-03-18 15:36:11 -04:00
mannilakash 115fcc67f6 fix: fetching game/grpc/websocket as well for statusPage 2026-03-17 15:14:44 -04:00
mannilakash 622446c308 fix: refactoring monitorsList and minor fixes based on feedback 2026-03-17 15:01:30 -04:00
mannilakash 294a149fd1 fix: minor changes to statusPageValidation and other fixes 2026-03-17 13:43:36 -04:00
mannilakash d3d00353a1 fix: minor refactoring infrastructureMetrics 2026-03-17 06:04:42 -04:00
mannilakash 894b6a7a7d feat: status-page infra section FE 2026-03-16 17:46:13 -04:00
mannilakash 3a9f9f0ce5 feat: status-page infrastructure section BE 2026-03-16 17:14:28 -04:00
Alexander Holliday 45618a2ab0 Merge pull request #3416 from bluewave-labs/fix/heatmap-response-time-fix
fix: modify heatmap response time conditional text
2026-03-13 12:00:13 -07:00
Alexander Holliday d80e232a10 Merge pull request #3418 from bluewave-labs/fix/middleware-typing
fix: middleware typing
2026-03-13 11:41:50 -07:00
ashu130698 1706012e54 fix:format code 2026-03-13 21:40:28 +05:30
Alex Holliday db9d5cf5e4 verifyJWT types 2026-03-12 11:25:32 -07:00
Alex Holliday 52a6b4c9fa overload sanitizeInput 2026-03-12 11:17:39 -07:00
Alex Holliday 9646e7dbdc type handleErrors 2026-03-12 11:12:46 -07:00
mannilakash d6ceedc2dd fix: upstream changes for resolving invalid responseTime 2026-03-12 14:05:31 -04:00
Alexander Holliday 501664509b Merge pull request #3417 from bluewave-labs/fix/check-snapshot
fix: properly type snapshot
2026-03-12 10:51:38 -07:00
Alex Holliday 2ef184c58a properly type snapshot 2026-03-12 10:43:18 -07:00
Alexander Holliday 22f917034d Merge pull request #3415 from bluewave-labs/fix/unset-with-explicit-null
fix: unset with explicit null
2026-03-12 10:12:40 -07:00
Alex Holliday 81f623b632 properly type updates 2026-03-12 10:05:36 -07:00
Alex Holliday d3605b0149 update client validation/pages 2026-03-12 09:54:08 -07:00
Alex Holliday 48e71d4345 use null for unsetting instead of empty string 2026-03-12 09:53:31 -07:00
mannilakash 411270d256 fix: modify heatmap response time conditional text 2026-03-12 12:44:46 -04:00
Alexander Holliday 6404a06999 Merge pull request #3414 from bluewave-labs/fix/settings-controller-typing
fix: SettingsController correct types
2026-03-12 09:38:56 -07:00
Alex Holliday a2bc2d8442 Merge branch 'fix/settings-controller-typing' of github.com:ajhollid/bluewave-uptime into fix/settings-controller-typing 2026-03-12 09:37:37 -07:00
Alex Holliday e9cb302d6e remove dead unions 2026-03-12 09:37:25 -07:00
Alex Holliday fa8764fde2 remove dead union, remove unused undefined -> conversion 2026-03-12 09:37:25 -07:00
Alex Holliday 84ca5db0fa refactor settingsController 2026-03-12 09:37:08 -07:00
Alex Holliday 8ec285ed41 remove dead unions 2026-03-12 09:35:33 -07:00
Alex Holliday d070a33d7f remove dead union, remove unused undefined -> conversion 2026-03-12 09:32:34 -07:00
Alex Holliday aa61e0f321 refacotr settingsController 2026-03-12 09:13:35 -07:00
ashu130698 88a39fe502 Merge branch 'upstream/develop' into feat/bulk-edit-notifications and resolve conflicts 2026-03-12 13:17:40 +05:30
Alexander Holliday a6b92d3c51 Merge pull request #3412 from bluewave-labs/fix/queueController-types
fix: QueueController Types
2026-03-11 14:10:26 -07:00
Alex Holliday 1af609a569 remove unsued route/methods 2026-03-11 14:08:20 -07:00
Alex Holliday 61cc0c2bef remove unused route 2026-03-11 13:51:37 -07:00
Alexander Holliday e4a228e61e Merge pull request #3411 from bluewave-labs/fix/notification-controller-validation
fix: implement NotificationController missing validation
2026-03-11 13:46:51 -07:00
Alex Holliday 76f550843b remove dead check 2026-03-11 13:45:17 -07:00
Alex Holliday 15d8a8ba1f fix test alert type 2026-03-11 13:40:35 -07:00
Alexander Holliday 842fa92cec Merge pull request #3401 from bluewave-labs/fix/rename-breached-to-exceeded
fix: rename "breached" status to "threshold exceeded"
2026-03-11 13:36:53 -07:00
Alex Holliday e878f51887 use service name 2026-03-11 13:30:39 -07:00
gorkem-bwl 9466090332 fix: rename "breached" to "threshold exceeded" in user-facing strings
Only changes user-facing text — no enum, type, or backend status changes.

## Changes
- locales/en.json: "breached" → "threshold exceeded"
- notificationMessageBuilder.ts: notification title/summary/details
- email.ts: email subject line
2026-03-11 16:30:30 -04:00
Alex Holliday 16b267d3a5 implement missing validation 2026-03-11 13:28:48 -07:00
Alexander Holliday 01f2aa294e Merge pull request #3410 from bluewave-labs/fix/monitor-controller-validation
fix: validation in monitor controller
2026-03-11 13:15:41 -07:00
Alex Holliday c156a49cc0 remove dead check 2026-03-11 13:10:09 -07:00
Alex Holliday 20ff76df9d remove unsued route/validation 2026-03-11 13:07:56 -07:00
Alex Holliday ff90953f17 cleanup and tighten validation 2026-03-11 12:57:27 -07:00
Alex Holliday 545926cfcf fix validation in monitor controller 2026-03-11 12:41:27 -07:00
Alexander Holliday 365b6cc60a Merge pull request #3409 from bluewave-labs/fix/mw-controller-typing
fix: type MWController
2026-03-11 12:11:09 -07:00
Alex Holliday 42f6c42ae1 fix discarded validation, tighten types 2026-03-11 12:04:28 -07:00
Alex Holliday 456237ba4e tighten date typing 2026-03-11 12:00:19 -07:00
Alex Holliday e887c2308a type MWController 2026-03-11 11:51:01 -07:00
Alexander Holliday 24d571a8db Merge pull request #3408 from bluewave-labs/fix/logController
fix: log controller
2026-03-11 11:39:01 -07:00
Alexander Holliday d41c5e9ebf Merge pull request #3407 from bluewave-labs/fix/invite-controller-typing
fix: inviteController typing
2026-03-11 11:38:02 -07:00
Alex Holliday d07f259829 properly type logController 2026-03-11 11:37:17 -07:00
Alex Holliday 566011c4e9 fix inviteController typing 2026-03-11 11:35:36 -07:00
Alexander Holliday 3dd0e0a998 Merge pull request #3403 from bluewave-labs/fix/email-body-mjml
fix: empty email body due to TSC ignoring .mjml templates
2026-03-11 11:31:24 -07:00
Alexander Holliday cda0ec3a4c Merge pull request #3406 from bluewave-labs/fix/incident-controller-typing
fix: Incident Controller/Service typing
2026-03-11 11:18:05 -07:00
Alex Holliday d00889ff9b renamed variable 2026-03-11 11:14:44 -07:00
Alex Holliday eafd97b9ad controller, service, validation 2026-03-11 11:10:38 -07:00
Alexander Holliday 7a0e0bccf4 Merge pull request #3405 from bluewave-labs/fix/diagnostic-controller-types
fix: properly type diagnosticController
2026-03-11 10:51:42 -07:00
Alexander Holliday 01401d56b8 Merge pull request #3404 from bluewave-labs/fix/controller-utils-types
fix: controller utils types
2026-03-11 10:51:11 -07:00
Alex Holliday f69b7f6a35 properly type diagnosticController 2026-03-11 10:50:10 -07:00
Alex Holliday cb5316e6f5 fix ssl-checker type 2026-03-11 10:48:12 -07:00
Alexander Holliday 25baff3013 Merge pull request #3400 from bluewave-labs/fix/dns-cache-ttl
fix: set DNS cache TTL to prevent stale lookups
2026-03-11 10:36:08 -07:00
mannilakash bcff3e15a8 fix: updating build script to add .mjml templates 2026-03-11 13:34:25 -04:00
mannilakash 1ad39fcac6 fix: empty email body due to TSC ignoring .mjml templates 2026-03-11 12:42:57 -04:00
Gorkem Cetin 85d2d34a90 Fix links to Checkmate documentation and user guide
Updated links to documentation and user guides in README.
2026-03-11 02:35:54 -04:00
Gorkem Cetin f6d9b7566b Remove project references from README
Removed references to other BlueWave projects from the README.
2026-03-11 01:47:48 -04:00
gorkem-bwl 943ef92053 fix: rename "breached" status to "exceeded" for clarity
The word "breached" in infrastructure monitoring was confusing users
as it implied a security breach. Renamed to "exceeded" which better
describes threshold violations (e.g. "threshold exceeded").

## Changes
- Renamed monitor status value from "breached" to "exceeded" across
  server types, status service, queue helper, notification builder,
  email subject lines, and MongoDB aggregation queries
- Renamed client types, status box/label components, translation key,
  and infrastructure page props to match
- Added DB migration (0005) to update existing monitors with the old
  status value
2026-03-11 01:31:05 -04:00
gorkem-bwl 84cac870d5 fix: set DNS cache TTL to prevent stale lookups
CacheableLookup defaults to maxTtl=Infinity, meaning successful DNS
lookups are cached forever until process restart. This causes monitors
to fail permanently if DNS changes or if a resolver outage causes
ENOTFOUND errors to persist after recovery.

Set maxTtl=300s (5 min) for successful lookups and errorTtl=30s for
failed lookups so the cache recovers automatically.
2026-03-10 19:04:04 -04:00
Alex Holliday 2884ab524d type fetchMonitorCertificate 2026-03-10 11:35:05 -07:00
Alexander Holliday eac470cb68 Merge pull request #3399 from bluewave-labs/fix/auth-controller
fix: type authController correctly, update services accordingly
2026-03-10 11:31:57 -07:00
Alex Holliday a15b9837c6 fix getUserById 2026-03-10 11:29:40 -07:00
Alex Holliday 562d723fcf remove redundant checks, use validated body 2026-03-10 11:25:55 -07:00
Alex Holliday 2fdd5a72f0 type authController correctly, update services accordingly 2026-03-10 11:18:59 -07:00
ashu130698 f9a4659cb2 Merge branch 'develop' into feat/bulk-edit-notifications 2026-03-10 23:27:01 +05:30
Alexander Holliday 3e26387b9d Merge pull request #3398 from bluewave-labs/feat/retention
feat: retention
2026-03-10 10:45:24 -07:00
Alex Holliday dc7d442ace remove unnecessary check 2026-03-10 10:44:02 -07:00
Alex Holliday 839f1beb41 remove __v from client type 2026-03-10 10:37:44 -07:00
Alex Holliday bbdadb0bf5 remove expiry from repository and service 2026-03-10 10:34:09 -07:00
Alex Holliday cadf68740a remove old expiry field, remove vestigal checks 2026-03-10 10:33:18 -07:00
ashu130698 0fc338db2f refactor(backend): move notifications update endpoint to monitors route and handle security feedback 2026-03-10 22:57:55 +05:30
Alex Holliday 6ad0a6a29d validation fix, batch deleteOlderThan 2026-03-10 10:20:23 -07:00
Alex Holliday 77af9d2e93 FE UI, userService type fix 2026-03-09 11:32:01 -07:00
Alex Holliday b1147b8bf9 FE implementation 2026-03-09 11:28:46 -07:00
Alex Holliday 5ab230f77b remove any types 2026-03-09 11:15:50 -07:00
Alex Holliday d9cb81e71d backend implementation 2026-03-09 11:14:40 -07:00
Alexander Holliday 0058772f61 Merge commit from fork
Fixed the Mass Assignment Privilege Escalation in Checkmate
2026-03-09 10:27:39 -07:00
Alexander Holliday bbc2e4073b Merge pull request #3375 from tb102122/feature/teams-notification-channel
add teams
2026-03-09 09:39:56 -07:00
Alexander Holliday 7967b363c2 Merge pull request #3395 from bluewave-labs/fix/demo-monitors
fix: demo monitor import
2026-03-09 09:10:27 -07:00
Aman Rawat 27dffe9fa6 Fixed 2026-03-09 13:12:01 +05:30
tobias.bruckert@mcon-group.com 7213bace08 Merge branch 'feature/teams-notification-channel' of https://github.com/tb102122/Checkmate into feature/teams-notification-channel 2026-03-09 08:43:30 +11:00
tobias.bruckert@mcon-group.com 2cbc667f30 add type checks and remove unneeded vars 2026-03-09 08:43:14 +11:00
Tobias Bruckert 978dc4cda6 Merge branch 'develop' into feature/teams-notification-channel 2026-03-09 08:33:36 +11:00
ashu130698 5578fdf17f Revert "feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors"
This reverts commit 27dd565bc6.
2026-03-07 10:40:38 +05:30
Alex Holliday a960e3aba2 fix demo monitor import 2026-03-06 14:27:57 -08:00
Alexander Holliday ff1d473b81 Merge pull request #3392 from bluewave-labs/feat/version-bump
bump bersion
2026-03-06 12:48:48 -08:00
Alex Holliday bd384bde4a bump version 2026-03-06 12:47:00 -08:00
Alexander Holliday e7f052dfa0 Merge pull request #3386 from bluewave-labs/feat/remove-team-member
Add ability to remove team members
2026-03-06 12:37:55 -08:00
Alex Holliday 9903c72ff5 merge 2026-03-06 12:36:26 -08:00
Alex Holliday d7d237d37b move button strings to correct location, flex-end 2026-03-06 12:35:38 -08:00
Alexander Holliday 5d03f6f0f0 Merge pull request #3391 from ajhollid/fix/bump-zod
explicitly bump zod
2026-03-06 12:27:34 -08:00
Alex Holliday 38e2a64255 explicitly bump zod 2026-03-06 12:25:32 -08:00
Alexander Holliday 1f56338697 Merge pull request #3390 from bluewave-labs/fix/port-env-var
Fix: restore PORT env var
2026-03-06 12:20:02 -08:00
gorkem-bwl 172467c0a6 fix: address PR review feedback
- Use useIsAdmin and useIsSuperAdmin hooks instead of manual role checks
- Close delete dialog on failure, not just on success
2026-03-06 15:19:01 -05:00
Alexander Holliday ff2b6dff2b Merge pull request #3387 from bluewave-labs/feat/uptime-websockets
Feat: Uptime - Websockets
2026-03-06 12:12:11 -08:00
Br0wnHammer 9f600f87af Fix: Dev Dependencies in Server 2026-03-07 01:40:07 +05:30
Br0wnHammer 7eeff7a396 Fix: Dev Dependencies 2026-03-07 01:37:37 +05:30
Alex Holliday afd6a12e7d restore PORT env var 2026-03-06 12:07:18 -08:00
Br0wnHammer 125d70c4b1 Format Checks 2026-03-07 01:13:00 +05:30
Br0wnHammer 5ebe46a81c Feat: Websocket Implementation 2026-03-07 01:11:20 +05:30
Br0wnHammer 19174ab3e5 Feat: update filter controls and default types 2026-03-07 01:02:30 +05:30
gorkem-bwl 7c953978ed refactor: simplify EditUser imports and canDeleteUser logic
- Merge duplicate react-router-dom imports into one
- Replace useIsAdmin + useIsSuperAdmin + useSelector with single useSelector
- Remove redundant !includes("superadmin") check from canDeleteUser
2026-03-06 14:31:59 -05:00
Br0wnHammer db5441d0e2 Feat: Frontend - update form hook and create/edit page 2026-03-07 01:01:49 +05:30
Br0wnHammer 1d529e3be4 Feat: Frontend - Add websocket to client types and validations 2026-03-07 01:00:38 +05:30
Br0wnHammer 23247d4319 Feat: Update server validation 2026-03-07 00:59:39 +05:30
Br0wnHammer 216d9b2996 Feat: Backend - Config 2026-03-07 00:58:55 +05:30
Br0wnHammer 62f1e62a26 Feat: Backend service - Implement WebSocket check in NetworkService 2026-03-07 00:57:52 +05:30
Br0wnHammer 55bdae77f9 Feat: Backend types - Register websocket type and payload 2026-03-07 00:56:58 +05:30
gorkem-bwl 82998a7795 feat: add ability to remove team members
## Changes
- Add DELETE /api/v1/auth/users/:userId endpoint (admin + superadmin)
- Add deleteUserById service method with role hierarchy enforcement
- Superadmins can remove admins and regular users
- Admins can only remove regular users
- No one can remove superadmins, demo users, or themselves via this endpoint
- Add "Remove user" button with confirmation dialog on edit user page
- Allow admins to view user details and access the edit user page
- Add i18n strings for remove user button and confirmation dialog

## Access control
- superadmin -> can remove admin, user
- admin -> can remove user only
- admin cannot remove superadmin or other admins (enforced on both backend and frontend)
2026-03-06 14:18:27 -05:00
Alexander Holliday 9fe8b7572f Merge pull request #3374 from bluewave-labs/fix/dark-mode-background-color
Fix dark mode background to use neutral gray
2026-03-06 11:14:21 -08:00
Alex Holliday a424092fbd remove unused imports 2026-03-06 11:12:52 -08:00
Alexander Holliday ad427dc765 Merge pull request #3385 from bluewave-labs/feat/globalping-ping-type
feat: globalping ping type
2026-03-06 11:10:45 -08:00
Alex Holliday 116eb80fc9 format 2026-03-06 11:07:48 -08:00
Alex Holliday c094a5fd73 format 2026-03-06 11:02:20 -08:00
Alex Holliday 206e28d1b9 user helper 2026-03-06 11:01:38 -08:00
Alex Holliday 2f1a164c26 remove rtt 2026-03-06 10:46:00 -08:00
Alex Holliday 124a8c0b07 ping type globalping checks 2026-03-06 10:37:29 -08:00
Alexander Holliday 188445c57a Merge pull request #3384 from bluewave-labs/fix/prettier
fix prettier hook
2026-03-06 09:46:08 -08:00
Alex Holliday 35f018623f fix prettier hook 2026-03-06 09:45:33 -08:00
Alexander Holliday d64526fed2 Merge pull request #3383 from bluewave-labs/fix/providers
fix: providers
2026-03-06 09:39:14 -08:00
Alex Holliday 9a8a175f7c format 2026-03-06 09:37:45 -08:00
Alex Holliday 6ecb27d522 remove buildStatusResponse 2026-03-06 09:35:52 -08:00
Alex Holliday 24b9b64979 manually build response 2026-03-06 09:35:07 -08:00
Alexander Holliday a4e4e1313e Merge pull request #3382 from bluewave-labs/fix/ts-errors
fix: resolve ts errors
2026-03-06 09:19:02 -08:00
Alex Holliday 990dc7875a format 2026-03-06 09:16:22 -08:00
Alex Holliday 69e98a51ec resolve ts errors 2026-03-06 09:14:12 -08:00
ashu130698 2a1645baf2 Merge branch 'develop' from upstream into feat/bulk-edit-notifications 2026-03-06 21:42:39 +05:30
ashu130698 8483a7bc55 fix(validation): remove invalid 2026-03-06 21:10:57 +05:30
ashu130698 27dd565bc6 feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors 2026-03-06 21:07:29 +05:30
ashu130698 c5f0316990 fix(validation): remove invalid zod enum argument 2026-03-06 20:41:33 +05:30
tobias.bruckert@mcon-group.com 08fd09f59b fix build errors 2026-03-06 19:48:19 +11:00
ashu130698 a40a76fb3c fix: resolve Zod build error and run formatter 2026-03-06 13:22:32 +05:30
tobias.bruckert@mcon-group.com 6c421717f8 run formatting 2026-03-06 17:58:04 +11:00
tobias.bruckert@mcon-group.com 083db95151 Merge branch 'feature/teams-notification-channel' of https://github.com/tb102122/Checkmate into feature/teams-notification-channel 2026-03-06 17:49:08 +11:00
tobias.bruckert@mcon-group.com b11ba68ff8 adjust based on review 2026-03-06 17:47:35 +11:00
Tobias Bruckert 12a4112055 Merge branch 'develop' into feature/teams-notification-channel 2026-03-06 13:08:41 +11:00
Alexander Holliday 3063b9cf22 Merge pull request #3379 from bluewave-labs/fix/network-providers
fix: network providers
2026-03-05 14:52:33 -08:00
Alex Holliday bd32854c7a cleanup services 2026-03-05 14:46:28 -08:00
Alex Holliday 0718e63e2f cleanup network service 2026-03-05 14:11:11 -08:00
Alexander Holliday cdb70f9171 Merge pull request #3378 from bluewave-labs/fix/email
Fix/email
2026-03-05 14:01:03 -08:00
Alex Holliday 69e07975e3 bump dependencies 2026-03-05 13:57:45 -08:00
Alex Holliday c5c9a1a88b fix missing await 2026-03-05 13:57:39 -08:00
Alexander Holliday 31efa791bd Merge pull request #3377 from bluewave-labs/fix/providers
fix/providers
2026-03-05 13:23:27 -08:00
Alex Holliday f8c0b57d21 remove unused imports 2026-03-05 19:37:32 +00:00
Alex Holliday 605c969d2f merge commit 2026-03-05 19:37:03 +00:00
Alex Holliday ad7d4e54c3 cleanup 2026-03-05 19:34:57 +00:00
Alex Holliday c7689cc64a grpc 2026-03-05 19:18:29 +00:00
Alex Holliday 27c44a0fbd config 2026-03-05 18:48:19 +00:00
Alex Holliday 190d9dd2b1 gameProvider 2026-03-05 18:45:27 +00:00
Alex Holliday 49672db563 port provider 2026-03-05 18:35:19 +00:00
Alex Holliday 80603c9a42 port provider 2026-03-05 18:30:06 +00:00
Alex Holliday f3b156f2d0 docker provider 2026-03-05 18:19:03 +00:00
ashu130698 4d7de2d3a1 fix: resolve merge conflicts, migrate validation to zod 2026-03-05 23:40:49 +05:30
Alex Holliday ec82efebb4 pagespeed provider, hardware provider 2026-03-05 17:54:16 +00:00
ashu130698 9c17fbe79c refactor: add Joi validation, rename to updateNotifications, add default case 2026-03-05 23:06:41 +05:30
ashu130698 8eca81c8be fix: handle malformed IDs and allow empty set action 2026-03-05 18:02:55 +05:30
ashu130698 eb2d8b8d3b style: format notificationController with prettier 2026-03-05 13:58:53 +05:30
tobias.bruckert@mcon-group.com 33201096b7 add teams 2026-03-05 19:16:08 +11:00
ashu130698 4827f8faec refractor: simplify bulk edit route to PATCH /notifications 2026-03-05 13:06:36 +05:30
gorkem-bwl a1068f1d7e refactor: move dark background colors to colors object
Addresses review feedback — define gray900 and gray850 in the colors
object and reference them in the dark palette.
2026-03-04 19:56:54 -05:00
Alex Holliday 8dd846d6cd harden http provider 2026-03-04 23:36:36 +00:00
Alex Holliday 82175f166f pingProvider, httpProvider 2026-03-04 23:24:48 +00:00
Alex Holliday 3c99809e8e rix reveresed checks 2026-03-04 23:24:05 +00:00
Alexander Holliday 26db2b3a41 Merge pull request #3371 from bluewave-labs/fix/infrastructure-threshold-migration
Fix infrastructure threshold defaults via migration (#3327)
2026-03-04 14:28:49 -08:00
Alexander Holliday 232a71c502 Merge pull request #3369 from bluewave-labs/fix/swagger-dynamic-server-url
fix: dynamically set Swagger UI server URL from request
2026-03-04 14:28:06 -08:00
gorkem-bwl 8df263fd93 fix: also migrate threshold value of 0 to 100, fix formatting
Addresses review feedback — thresholds set to 0 also caused issues
and should be migrated to 100 (disabled) alongside the value of 5.
2026-03-04 16:44:20 -05:00
gorkem-bwl d2f3335b1a fix: add type annotations to Swagger middleware parameters
Fixes TypeScript build error TS7006 for implicit 'any' types on req, res, next.
2026-03-04 16:32:01 -05:00
gorkem-bwl 75eca743b4 fix: use neutral gray background for dark mode
## Changes
- Replace MUI default blue-tinted dark background with neutral grays
  (#151518 for default, #1c1c21 for paper/cards)
- Remove decorative blue/purple SVG blob overlay from dark mode background

## Benefits
- Dark mode now has a clean, neutral gray appearance instead of a blue tint
2026-03-04 16:30:19 -05:00
Alexander Holliday 7bfd68e374 Merge pull request #3372 from bluewave-labs/fix/ipv6-support
Fix IPv6 address handling in ping monitor (#3053)
2026-03-04 13:00:15 -08:00
gorkem-bwl b2e2b11ef2 fix: support IPv6 addresses in ping monitor
## Changes
- Fixed ping host sanitization regex that stripped everything after
  the first colon, which destroyed IPv6 addresses like 2001:db8::1
- New regex properly handles bracketed IPv6 ([::1]:port), bare IPv6,
  and IPv4/hostname:port formats separately

## Closes
Fixes #3053
2026-03-04 15:41:01 -05:00
gorkem-bwl 93b9abd508 fix: add migration to reset infrastructure thresholds from 5 to 100
## Changes
- Added migration 0004_fixInfrastructureThresholds that finds hardware
  monitors with threshold values of 5 (the old incorrect default) and
  updates them to 100 (effectively disabled)
- Runs automatically on server startup like other migrations

## Closes
Fixes #3327
2026-03-04 15:38:34 -05:00
gorkem-bwl 888b399a4f fix: dynamically set Swagger UI server URL from request
## Changes
- Swagger UI now prepends the current server URL (protocol + host) to
  the servers list, so it works correctly in remote/Docker environments
  instead of always pointing to localhost.

## Closes
Fixes #3339
2026-03-04 15:29:44 -05:00
ashu130698 9e06758997 added a PATCH/notifications/bulk/monitor 2026-03-03 23:28:11 +05:30
290 changed files with 42185 additions and 20511 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
* text=auto
* text eol=lf
+7 -6
View File
@@ -1,27 +1,28 @@
**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
## Describe your changes
Briefly describe the changes you made and their purpose.
Briefly describe the changes you made and their purpose.
## Write your issue number after "Fixes "
Fixes #123
Fixes #123
## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.
- [ ] (Do not skip this or your PR will be closed) I deployed the application locally.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-reviewing and testing of my code.
- [ ] I have included the issue # in the PR.
- [ ] I have added i18n support to visible strings (instead of `<div>Add</div>`, use):
- [ ] I have added i18n support to visible strings (instead of `<div>Add</div>`, use):
```Javascript
const { t } = useTranslation();
<div>{t('add')}</div>
```
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
+23
View File
@@ -0,0 +1,23 @@
name: Test Check (Server)
on:
pull_request:
workflow_dispatch:
jobs:
test-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install server dependencies
working-directory: server
run: npm install
- name: Run server tests
working-directory: server
run: npm test
+122 -104
View File
@@ -1,120 +1,138 @@
name: POEditor Translation Synchronization
name: POEditor Translation Synchronization (disabled)
# This workflow is intentionally disabled — kept for reference only.
# The full definition is preserved commented-out below. To re-enable,
# delete the stub job and uncomment the section starting at `on:`.
on:
# For manual triggering
workflow_dispatch:
inputs:
languages:
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
required: false
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
format:
description: "Export format (key_value_json or json)"
required: false
default: "key_value_json"
permissions:
contents: write
pull-requests: write
workflow_dispatch:
jobs:
sync-translations:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
disabled:
if: false
runs-on: ubuntu-latest
steps:
- run: echo "This workflow is disabled. See comments below for the original definition."
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
# ---------------------------------------------------------------------------
# Original (disabled) workflow below. Do not uncomment if you don't know what you're doing.
# ---------------------------------------------------------------------------
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
# on:
# # For manual triggering
# workflow_dispatch:
# inputs:
# languages:
# description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
# required: false
# default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
# format:
# description: "Export format (key_value_json or json)"
# required: false
# default: "key_value_json"
- name: Create package.json for scripts
run: |
mkdir -p .github/scripts
cat > .github/scripts/package.json << EOF
{
"name": "poeditor-scripts",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"axios": "^1.6.0",
"fs-extra": "^11.1.1"
}
}
EOF
# permissions:
# contents: write
# pull-requests: write
- name: Install dependencies
run: |
cd .github/scripts
npm install
# jobs:
# sync-translations:
# runs-on: ubuntu-latest
# permissions:
# contents: write
# issues: write
# pull-requests: write
- name: Download translations from POEditor
env:
POEDITOR_API: ${{ secrets.POEDITOR_API }}
POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
run: |
mkdir -p temp
node .github/scripts/download-translations.js
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
# fetch-depth: 0
- name: Verify translation files
run: |
echo "Verifying translation files..."
for file in temp/*.json; do
echo "Checking $file"
if [ ! -s "$file" ]; then
echo "Error: $file is empty or does not exist"
exit 1
fi
# Validate JSON format
cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
done
echo "All translation files are valid"
# - name: Setup Node.js
# uses: actions/setup-node@v3
# with:
# node-version: "22"
- name: Copy translations to project
run: |
mkdir -p client/src/locales
cp -r temp/* client/src/locales/
echo "Translation files copied to client/src/locales/"
# - name: Create package.json for scripts
# run: |
# mkdir -p .github/scripts
# cat > .github/scripts/package.json << EOF
# {
# "name": "poeditor-scripts",
# "version": "1.0.0",
# "private": true,
# "type": "module",
# "dependencies": {
# "axios": "^1.6.0",
# "fs-extra": "^11.1.1"
# }
# }
# EOF
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
# - name: Install dependencies
# run: |
# cd .github/scripts
# npm install
- name: Format client code
run: |
cd client
npm ci
npm run format
# - name: Download translations from POEditor
# env:
# POEDITOR_API: ${{ secrets.POEDITOR_API }}
# POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
# LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
# EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
# run: |
# mkdir -p temp
# node .github/scripts/download-translations.js
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "feat: translations updated from POEditor"
title: "🌐 Updated Translations from POEditor"
body: |
This PR contains the latest translations from POEditor.
# - name: Verify translation files
# run: |
# echo "Verifying translation files..."
# for file in temp/*.json; do
# echo "Checking $file"
# if [ ! -s "$file" ]; then
# echo "Error: $file is empty or does not exist"
# exit 1
# fi
# # Validate JSON format
# cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
# done
# echo "All translation files are valid"
📅 Update Date: ${{ steps.date.outputs.date }}
🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
# - name: Copy translations to project
# run: |
# mkdir -p client/src/locales
# cp -r temp/* client/src/locales/
# echo "Translation files copied to client/src/locales/"
⚠️ Please review the translations and approve the PR if everything looks correct.
branch: translation-update-${{ github.run_number }}
delete-branch: true
base: develop
add-paths: |
client/src/locales/*.json
committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
author: GitHub Action <github-actions[bot]@users.noreply.github.com>
# - name: Get current date
# id: date
# run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
# - name: Format client code
# run: |
# cd client
# npm ci
# npm run format
# - name: Create Pull Request
# uses: peter-evans/create-pull-request@v7
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
# commit-message: "feat: translations updated from POEditor"
# title: "🌐 Updated Translations from POEditor"
# body: |
# This PR contains the latest translations from POEditor.
# 📅 Update Date: ${{ steps.date.outputs.date }}
# 🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
# ⚠️ Please review the translations and approve the PR if everything looks correct.
# branch: translation-update-${{ github.run_number }}
# delete-branch: true
# base: develop
# add-paths: |
# client/src/locales/*.json
# committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
# author: GitHub Action <github-actions[bot]@users.noreply.github.com>
+3 -2
View File
@@ -2,7 +2,7 @@ name: Demo deploy
on:
push:
branches: ["demo"]
branches: [ "demo" ]
workflow_dispatch:
jobs:
docker-build-and-push-client:
@@ -84,7 +84,8 @@ jobs:
script: |
cd checkmate
git pull
cd docker/prod
cd docker/unified
docker compose down
docker compose pull
docker compose up -d
docker system prune -af
+5 -5
View File
@@ -2,7 +2,7 @@ name: Staging deploy
on:
push:
branches: ["develop"]
branches: [ "develop" ]
jobs:
docker-build-and-push-client:
@@ -78,13 +78,13 @@ jobs:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.STAGING_SERVER_HOST }}
username: ${{ secrets.STAGING_SERVER_USER }}
key: ${{ secrets.STAGING_SERVER_SSH_KEY }}
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docker/staging
cd docker/unified
docker compose down
docker compose pull
docker compose up -d
+51 -1
View File
@@ -105,6 +105,56 @@ client/src/
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
- **i18n**: i18next + react-i18next (translations via PoEditor)
---
## Backend Architecture Patterns
### Repository Pattern & Separation of Concerns
The backend enforces a strict three-layer separation between HTTP handling, business logic, and data access:
```
Request → Controller → Service → Repository → MongoDB (Mongoose)
```
- **Controllers** (`/controllers`) handle HTTP concerns only: parsing request params, calling the appropriate service, and returning a response via the `responseHandler` middleware. They contain no business logic.
- **Services** (`/service/business`) contain all business logic: deciding whether an incident should be created, whether a notification should fire, what state a monitor is in, etc.
- **Repositories** (`/repositories`) are the sole layer that talks to MongoDB through Mongoose. They expose clean, reusable query methods (e.g. `findByMonitorId`, `createCheck`) so that services never construct raw DB queries directly.
This separation makes each layer independently testable and keeps Mongoose-specific code out of business logic. When adding a new feature, the pattern to follow is: add a repository method for any new DB query, call it from a service, and expose it via a controller route.
### Monitoring Flow: From Check to Notification
Background monitoring runs on a scheduled queue, not on the HTTP request cycle. The high-level flow for uptime monitoring is:
```
Pulse (cron) → BullMQ Job → StatusService
├── performs HTTP/port/ping check
├── saves Check via CheckRepository
├── evaluates monitor state change
│ └── calls IncidentService (create / resolve incident)
└── calls NotificationService (email, Slack, Discord, webhook)
```
1. **Pulse** (cron scheduler) enqueues a job into a **BullMQ** queue for each active monitor at its configured interval.
2. A **BullMQ worker** picks up the job and calls `StatusService`, which performs the actual check (HTTP request, TCP port probe, ping, etc.).
3. The result is persisted as a `Check` document via the repository layer.
4. `StatusService` compares the new result against the monitor's previous state. If the monitor transitions from up → down (or down → up), it delegates to `IncidentService` to open or resolve an `Incident` document.
5. On a state change, `NotificationService` reads the monitor's configured `Notification` documents and dispatches alerts to all enabled channels (email, Discord, Slack, webhooks).
### Queue System (BullMQ + Redis)
Redis serves two roles: job queue storage for BullMQ and ephemeral caching. BullMQ manages concurrency, retries, and backpressure for monitoring jobs, ensuring checks are processed reliably even under load.
- Each monitor type (HTTP, port, ping, infrastructure) maps to its own queue worker so failures in one type don't block others.
- Job scheduling interval is driven by the `interval` field on the `Monitor` model.
- Failed jobs are retried with configurable backoff before being moved to a dead-letter state.
- Redis is also used to cache frequently read data (e.g. aggregated stats) to reduce MongoDB query pressure.
When working on anything related to check scheduling, incident lifecycle, or notifications, trace the flow starting from the relevant BullMQ worker rather than from the controller layer.
---
## Code Conventions
### Internationalization
@@ -142,4 +192,4 @@ Key Mongoose models in `/server/src/db/models/`:
- **StatusPage** - Public status pages
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
- **MaintenanceWindow** - Scheduled maintenance periods
- **AppSettings** - Global application settings
- **AppSettings** - Global application settings
+69 -69
View File
@@ -1,69 +1,69 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in the **bluewave-uptime** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, 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, especially during Hacktoberfest, where we encourage new contributors to join and feel supported.
## 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
## Our 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 with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project 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 by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly.
All project team members 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 inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
---
By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **bluewave-uptime** a positive space for all!
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in the **checkmate** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, 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, especially during Hacktoberfest, where we encourage new contributors to join and feel supported.
## 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
## Our 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 with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project 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 by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly.
All project team members 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 inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
---
By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **checkmate** a positive space for all!
+7 -1
View File
@@ -16,7 +16,7 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
Before you dive in, make sure you have these installed:
```bash
# Check Node.js (v16+ required)
# Check Node.js (v20+ required)
node --version
# Check npm
@@ -96,10 +96,16 @@ cd docker/dev
Build the Docker images:
Linux:
```bash
./build_images.sh
```
Windows:
```bash
./build_images.bat
```
Run MongoDB container:
```bash
+1 -1
View File
@@ -33,4 +33,4 @@ Ensure you execute `npm run format` before submitting your pull requests in both
### Keep PRs focused
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature.
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a bug or work on another feature.
+1 -1
View File
@@ -76,7 +76,7 @@ También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servido
Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
- [Discusiones en GitHub](https://github.com/bluewave-labs/checkmate/discussions)
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
+28 -24
View File
@@ -14,6 +14,8 @@
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate)
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
@@ -29,7 +31,6 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
- [📦 Demo](#demo)
- [🔗 User's guide](#users-guide)
- [🛠️ Installation](#installation)
- [🏁 Translations](#translations)
- [🚀 Performance](#performance)
- [💚 Questions & Ideas](#questions--ideas)
- [🧩 Features](#features)
@@ -42,17 +43,23 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
## Demo
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
You can see the latest build of [Checkmate](https://demo.checkmate.so/) in action.
The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
## User's guide
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
Usage instructions can be found [here](https://checkmate.so/docs).
## Prerequisites
- [Docker](https://www.docker.com/) installed
- [Git](https://git-scm.com/) installed
## Installation
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa), [Cloudzy](https://cloudzy.com/marketplace/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
### Using a Custom CA
@@ -60,11 +67,6 @@ If you need to monitor internal HTTPS endpoints with certificates from private C
For more documentation, see the [docs directory](./docs/).
## Translations
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
## Performance
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
@@ -80,7 +82,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
If you have any questions, suggestions or comments, you have several options:
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time)
Feel free to ask questions or share your ideas - we'd love to hear from you!
@@ -112,6 +114,15 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
## Monitor Lifecycle
1. A monitor executes a check (HTTP / ping / port / hardware via Capture agent)
2. The result is stored (success/failure + response time)
3. Recent check results are evaluated against the monitor's configured status change threshold
4. If the monitor's status change threshold is met and the current status is not equal to the previous status, the monitor's state changes (e.g. `initializing`, `up`, `down`, `breached`)
5. Upon a state change: an incident is either created or resolved, depending on the monitor's current status
6. Notifications are triggered based on configuration
## Screenshots
<p>
@@ -157,27 +168,20 @@ Here's how you can contribute:
0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
4. Open an issue if you believe you've encountered a bug.
5. Check for good-first-issue's if you are a newcomer.
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
3. Open an issue if you believe you've encountered a bug.
4. Check for good-first-issue's if you are a newcomer.
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date)
## Our sponsors
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
Also check other developer and contributor-friendly projects of BlueWave:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
+119 -168
View File
@@ -44,7 +44,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "^4.1.11"
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "24.5.2",
@@ -58,7 +58,7 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
"vite": "6.4.1"
}
},
"node_modules/@babel/code-frame": {
@@ -434,7 +434,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -451,7 +450,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -468,7 +466,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -485,7 +482,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -502,7 +498,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -519,7 +514,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -536,7 +530,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -553,7 +546,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -570,7 +562,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -587,7 +578,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -604,7 +594,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -621,7 +610,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -638,7 +626,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -655,7 +642,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -672,7 +658,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -689,7 +674,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -704,7 +688,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -721,7 +704,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -738,7 +720,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -755,7 +736,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -772,7 +752,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -789,7 +768,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -806,7 +784,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -823,7 +800,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -840,7 +816,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -857,7 +832,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1660,13 +1634,12 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1674,13 +1647,12 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1688,13 +1660,12 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1702,13 +1673,12 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1716,13 +1686,12 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1730,13 +1699,12 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1744,13 +1712,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1758,13 +1725,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1772,13 +1738,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1786,13 +1751,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1800,13 +1764,12 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1814,13 +1777,12 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1828,13 +1790,12 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1842,13 +1803,12 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1856,13 +1816,12 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1870,13 +1829,12 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1884,13 +1842,12 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1898,11 +1855,12 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1910,13 +1868,12 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1924,13 +1881,12 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1938,13 +1894,12 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1952,13 +1907,12 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1966,13 +1920,12 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1980,13 +1933,12 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1994,13 +1946,12 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2350,7 +2301,7 @@
},
"node_modules/@types/node": {
"version": "24.5.2",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.12.0"
@@ -2366,7 +2317,6 @@
},
"node_modules/@types/react": {
"version": "18.3.27",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2505,7 +2455,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3566,7 +3518,6 @@
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -3884,7 +3835,6 @@
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -4018,7 +3968,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -5251,7 +5200,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5317,7 +5268,6 @@
},
"node_modules/nanoid": {
"version": "3.3.11",
"dev": true,
"funding": [
{
"type": "github",
@@ -5650,7 +5600,6 @@
},
"node_modules/postcss": {
"version": "8.5.6",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6158,7 +6107,8 @@
},
"node_modules/rollup": {
"version": "4.57.1",
"dev": true,
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -6171,31 +6121,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
@@ -6530,7 +6480,6 @@
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6818,7 +6767,6 @@
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -6948,7 +6896,7 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -6992,7 +6940,7 @@
},
"node_modules/undici-types": {
"version": "7.12.0",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/union-value": {
@@ -7081,8 +7029,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"dev": true,
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@@ -7366,7 +7315,7 @@
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -7391,6 +7340,8 @@
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+3 -3
View File
@@ -13,7 +13,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "npm run format"
"**/*": "prettier --write"
},
"dependencies": {
"@emotion/react": "^11.13.3",
@@ -52,7 +52,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "^4.1.11"
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "24.5.2",
@@ -66,6 +66,6 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
"vite": "6.4.1"
}
}
+258
View File
@@ -0,0 +1,258 @@
/**
* Locale Migration Script
*
* Restructures all non-English locale files to match en.json's nested key structure.
* Recovers translations via safe key-matching strategies, uses English placeholders
* for unmatched keys.
*
* Usage: npx tsx client/scripts/migrate-locales.ts
*
* SAFETY: en.json is NEVER modified. Originals are backed up before overwriting.
*/
import fs from "fs";
import path from "path";
const LOCALES_DIR = path.resolve(import.meta.dirname, "../src/locales");
const BACKUP_DIR = path.join(LOCALES_DIR, "backup");
const REPORT_PATH = path.join(LOCALES_DIR, "migration-report.json");
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Flatten a nested object into { "a.b.c": value } pairs */
function flattenKeys(obj: Record<string, unknown>, prefix = ""): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const fullPath = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenKeys(value as Record<string, unknown>, fullPath));
} else {
result[fullPath] = String(value);
}
}
return result;
}
/** Set a value at a dot-separated path in a nested object */
function setNestedValue(
obj: Record<string, unknown>,
keyPath: string,
value: string
): void {
const parts = keyPath.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part] as Record<string, unknown>;
}
current[parts[parts.length - 1]] = value;
}
// ── Build mapping indexes from en.json ───────────────────────────────────────
function buildIndexes(enFlat: Record<string, string>) {
const enKeys = Object.keys(enFlat);
// last segment -> en key paths
const lastSegIndex: Record<string, string[]> = {};
// last 2 segments -> en key paths
const suffixIndex: Record<string, string[]> = {};
for (const keyPath of enKeys) {
const parts = keyPath.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
if (!lastSegIndex[lastSeg]) lastSegIndex[lastSeg] = [];
lastSegIndex[lastSeg].push(keyPath);
if (parts.length >= 2) {
const suffix = parts.slice(-2).join(".").toLowerCase();
if (!suffixIndex[suffix]) suffixIndex[suffix] = [];
suffixIndex[suffix].push(keyPath);
}
}
return { lastSegIndex, suffixIndex };
}
// ── Mapping logic ────────────────────────────────────────────────────────────
type MatchStrategy = "exact" | "suffix-2" | "unique-name" | "english-placeholder";
interface KeyMatch {
enKey: string;
value: string;
strategy: MatchStrategy;
}
function buildMigratedLocale(
enFlat: Record<string, string>,
langFlat: Record<string, string>,
indexes: ReturnType<typeof buildIndexes>
): { matches: KeyMatch[]; result: Record<string, unknown> } {
const { lastSegIndex, suffixIndex } = indexes;
const matches: KeyMatch[] = [];
const result: Record<string, unknown> = {};
// Build reverse lookup: for each orphan key in lang, index by last segment and suffix
const langByLastSeg: Record<string, { key: string; value: string }[]> = {};
const langBySuffix: Record<string, { key: string; value: string }[]> = {};
for (const [langKey, langValue] of Object.entries(langFlat)) {
const parts = langKey.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
if (!langByLastSeg[lastSeg]) langByLastSeg[lastSeg] = [];
langByLastSeg[lastSeg].push({ key: langKey, value: langValue });
if (parts.length >= 2) {
const suffix = parts.slice(-2).join(".").toLowerCase();
if (!langBySuffix[suffix]) langBySuffix[suffix] = [];
langBySuffix[suffix].push({ key: langKey, value: langValue });
}
}
for (const enKey of Object.keys(enFlat)) {
const enValue = enFlat[enKey];
const parts = enKey.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
const suffix = parts.length >= 2 ? parts.slice(-2).join(".").toLowerCase() : null;
// Strategy 1: Exact path match
if (langFlat[enKey] !== undefined) {
const value = langFlat[enKey];
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "exact" });
continue;
}
// Strategy 2: 2-segment suffix match (lang key suffix matches en key suffix, both unique)
if (suffix) {
const enCandidates = suffixIndex[suffix];
const langCandidates = langBySuffix[suffix];
if (
enCandidates &&
enCandidates.length === 1 &&
langCandidates &&
langCandidates.length === 1
) {
const value = langCandidates[0].value;
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "suffix-2" });
continue;
}
}
// Strategy 3: Unique last-segment match
const enCandidates = lastSegIndex[lastSeg];
const langCandidates = langByLastSeg[lastSeg];
if (
enCandidates &&
enCandidates.length === 1 &&
langCandidates &&
langCandidates.length === 1
) {
const value = langCandidates[0].value;
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "unique-name" });
continue;
}
// No match — use English placeholder
setNestedValue(result, enKey, enValue);
matches.push({ enKey, value: enValue, strategy: "english-placeholder" });
}
return { matches, result };
}
// ── Main ─────────────────────────────────────────────────────────────────────
function main() {
// Load en.json
const enPath = path.join(LOCALES_DIR, "en.json");
const en = JSON.parse(fs.readFileSync(enPath, "utf8"));
const enFlat = flattenKeys(en);
const enKeyCount = Object.keys(enFlat).length;
const indexes = buildIndexes(enFlat);
console.log(`English: ${enKeyCount} keys (source of truth — NOT modified)\n`);
// Find all non-English locale files
const localeFiles = fs
.readdirSync(LOCALES_DIR)
.filter(
(f: string) =>
f.endsWith(".json") && f !== "en.json" && f !== "migration-report.json"
);
if (localeFiles.length === 0) {
console.log("No locale files to migrate.");
return;
}
// Create backup directory
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
const report: Record<
string,
{
total: number;
exact: number;
suffix: number;
uniqueName: number;
placeholder: number;
}
> = {};
for (const file of localeFiles) {
const lang = file.replace(".json", "");
const filePath = path.join(LOCALES_DIR, file);
// Backup original
const backupPath = path.join(BACKUP_DIR, file);
fs.copyFileSync(filePath, backupPath);
// Load and flatten
const langData = JSON.parse(fs.readFileSync(filePath, "utf8"));
const langFlat = flattenKeys(langData);
// Migrate
const { matches, result } = buildMigratedLocale(enFlat, langFlat, indexes);
// Write migrated file
fs.writeFileSync(filePath, JSON.stringify(result, null, "\t") + "\n", "utf8");
// Compute stats
const stats = {
total: enKeyCount,
exact: matches.filter((m) => m.strategy === "exact").length,
suffix: matches.filter((m) => m.strategy === "suffix-2").length,
uniqueName: matches.filter((m) => m.strategy === "unique-name").length,
placeholder: matches.filter((m) => m.strategy === "english-placeholder").length,
};
report[lang] = stats;
const translated = stats.exact + stats.suffix + stats.uniqueName;
const pct = ((translated / enKeyCount) * 100).toFixed(1);
console.log(
`${lang.padEnd(8)} ${String(translated).padStart(4)} translated (${pct}%) | ` +
`exact: ${stats.exact} suffix: ${stats.suffix} name: ${stats.uniqueName} ` +
`placeholder: ${stats.placeholder}`
);
}
// Write report
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, "\t") + "\n", "utf8");
const totalLangs = localeFiles.length;
console.log(
`\nMigrated ${totalLangs} locale files. Originals backed up to locales/backup/`
);
console.log(`Report written to ${REPORT_PATH}`);
}
main();
@@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25).reverse();
const latestChecks = checks.slice(-25);
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
@@ -57,7 +57,8 @@ export const HeatmapResponseTimeTooltip = ({
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography>
<Typography>
{t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
{t("common.labels.responseTime")}:{" "}
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
</Typography>
<Typography textTransform={"capitalize"}>
Status:{" "}
+19 -3
View File
@@ -11,20 +11,26 @@ import type { ResponsiveStyleValue } from "@mui/system";
interface SliderInputProps extends SliderProps {
sx?: SliderProps["sx"];
showValue?: boolean;
formatDisplayValue?: (value: number) => string;
}
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
({ sx, showValue = false, ...props }, ref) => {
({ sx, showValue = false, formatDisplayValue, ...props }, ref) => {
const theme = useTheme();
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
const displayValue =
showValue && formatDisplayValue && typeof props.value === "number"
? formatDisplayValue(props.value)
: props.value;
return (
<Stack
gap={theme.spacing(8)}
direction={"row"}
alignItems={"center"}
>
{showValue && <Typography>{props.value}</Typography>}
{showValue && <Typography>{displayValue}</Typography>}
<Slider
{...props}
ref={ref}
@@ -63,12 +69,21 @@ interface SliderWithLabelProps extends SliderProps {
fieldLabel?: string;
required?: boolean;
showValue?: boolean;
formatDisplayValue?: (value: number) => string;
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
}
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
(
{ fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
{
fieldLabel,
required,
showValue = true,
formatDisplayValue,
value,
sliderMaxWidth = "100%",
...props
},
ref
) => {
const theme = useTheme();
@@ -82,6 +97,7 @@ export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>
<SliderInput
{...props}
showValue={showValue}
formatDisplayValue={formatDisplayValue}
value={value}
ref={ref}
/>
@@ -1,9 +1,6 @@
import Box from "@mui/material/Box";
import { useState, useEffect, useRef } from "react";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import BackgroundSVG from "@/assets/Images/background.svg";
import type { RootState } from "@/Types/state";
import { OfflineBanner } from "@/Components/design-elements";
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
@@ -13,7 +10,6 @@ interface AppLayoutProps {
const AppLayout = ({ children }: AppLayoutProps) => {
const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const [serverUnreachable, setServerUnreachable] = useState(false);
const retryIntervalRef = useRef<number | null>(null);
@@ -47,10 +43,6 @@ const AppLayout = ({ children }: AppLayoutProps) => {
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.background.default,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<OfflineBanner visible={serverUnreachable} />
@@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor";
import { Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
const types = ["http", "ping", "port", "docker", "game", "grpc"];
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
const typeDisplayNames: Record<string, string> = {
http: "HTTP",
ping: "Ping",
@@ -15,6 +15,7 @@ const typeDisplayNames: Record<string, string> = {
docker: "Docker",
game: "Game",
grpc: "gRPC",
websocket: "WebSocket",
};
const statuses = ["up", "down"];
const states = ["active", "paused"];
@@ -25,10 +25,7 @@ export const MonitorStatBoxes = ({
}
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt || 0).getTime();
const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0;
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
@@ -42,6 +39,12 @@ export const MonitorStatBoxes = ({
const streakTime = prettyMilliseconds(timeSinceLastFailure, options);
const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options);
const isActive =
monitor?.status === "up" ||
monitor?.status === "paused" ||
monitor?.status === "maintenance" ||
monitor?.status === "initializing" ||
monitor?.status === "breached";
const palette = getStatusPalette(monitor?.status);
return (
@@ -51,8 +54,12 @@ export const MonitorStatBoxes = ({
>
<StatBox
palette={palette}
title={t("pages.common.monitors.statBoxes.activeFor")}
subtitle={streakTime}
title={
isActive
? t("pages.common.monitors.statBoxes.activeFor")
: t("pages.common.monitors.statBoxes.serviceIsDown")
}
subtitle={isActive ? streakTime : ""}
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastCheck")}
+8
View File
@@ -103,6 +103,14 @@ export const useMonitorForm = ({
selectedDisks: data?.selectedDisks || [],
};
break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default:
defaults = {
...base,
+81 -15
View File
@@ -1,28 +1,94 @@
import { useMemo } from "react";
import { notificationSchema } from "@/Validation/notifications";
import type { NotificationFormData } from "@/Validation/notifications";
import type { Notification } from "@/Types/Notification";
interface UseNotificationFormOptions {
data?: Notification | null;
}
function buildDefaults(data: Notification | null): NotificationFormData {
if (data?.type === "matrix") {
return {
type: "matrix",
notificationName: data.notificationName || "",
homeserverUrl: data.homeserverUrl || "",
roomId: data.roomId || "",
accessToken: data.accessToken || "",
};
}
if (data?.type === "telegram") {
return {
type: "telegram",
notificationName: data.notificationName || "",
address: data.address || "",
accessToken: data.accessToken || "",
};
}
if (data?.type === "slack") {
return {
type: "slack",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "discord") {
return {
type: "discord",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "webhook") {
return {
type: "webhook",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "pager_duty") {
return {
type: "pager_duty",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "teams") {
return {
type: "teams",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "twilio") {
return {
type: "twilio",
notificationName: data.notificationName || "",
accountSid: data.accountSid || "",
accessToken: data.accessToken || "",
phone: data.phone || "",
twilioPhoneNumber: data.twilioPhoneNumber || "",
};
}
if (data?.type === "pushover") {
return {
type: "pushover",
notificationName: data.notificationName || "",
address: data.address || "",
accessToken: data.accessToken || "",
};
}
// Default: email (covers both data === null and data.type === "email")
return {
type: "email",
notificationName: data?.notificationName || "",
address: data?.address || "",
};
}
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
return useMemo(() => {
const defaults =
data?.type === "matrix"
? {
type: "matrix" as const,
notificationName: data.notificationName || "",
homeserverUrl: data.homeserverUrl || "",
roomId: data.roomId || "",
accessToken: data.accessToken || "",
}
: {
type: (data?.type || "email") as Exclude<Notification["type"], "matrix">,
notificationName: data?.notificationName || "",
address: data?.address || "",
};
const defaults = buildDefaults(data);
return { schema: notificationSchema, defaults };
}, [data]);
};
+2 -2
View File
@@ -1,14 +1,14 @@
import { useMemo } from "react";
import { settingsSchema } from "@/Validation/settings";
import type { Settings } from "@/Types/Settings";
import type { SettingsFormData } from "@/Validation/settings";
import type { SettingsFormInput } from "@/Validation/settings";
interface UseSettingsFormOptions {
data?: Settings | null;
}
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
return useMemo(() => {
const defaults: SettingsFormData = {
const defaults: SettingsFormInput = {
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
+2
View File
@@ -27,12 +27,14 @@ export const useStatusPageForm = ({
companyName: data?.companyName || "",
url: data?.url || generateDefaultUrl(),
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
type: data?.type || ["uptime"],
color: data?.color || "#4169E1",
monitors: data?.monitors || [],
isPublished: data?.isPublished ?? false,
showCharts: data?.showCharts ?? true,
showUptimePercentage: data?.showUptimePercentage ?? true,
showAdminLoginLink: data?.showAdminLoginLink ?? false,
showInfrastructure: data?.showInfrastructure ?? false,
customCSS: data?.customCSS || "",
logo: transformLogo(data?.logo),
};
+64 -11
View File
@@ -11,12 +11,17 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
import { UserRoles } from "@/Types/User";
import { useTranslation } from "react-i18next";
import type { UserRole, User } from "@/Types/User";
import { useParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material";
import { useEditUserForm } from "@/Hooks/useEditUserForm";
import { useGet, usePatch } from "@/Hooks/UseApi";
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
import type { EditUserFormData } from "@/Validation/editUser";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { DialogInput } from "@/Components/inputs/Dialog";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
interface RoleOption {
id: UserRole;
@@ -29,6 +34,12 @@ const EditUserPage = () => {
const { userId } = useParams<{ userId: string }>();
const { resolver, defaults } = useEditUserForm();
const { patch, loading: isSaving } = usePatch();
const { deleteFn, loading: isDeleting } = useDelete();
const navigate = useNavigate();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const currentUser = useSelector((state: RootState) => state.auth.user);
const isSuperAdmin = useIsSuperAdmin();
const isAdmin = useIsAdmin();
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
@@ -60,6 +71,20 @@ const EditUserPage = () => {
setValue("role", newRoles, { shouldValidate: true });
};
const canDeleteUser =
isAdmin &&
userId !== currentUser?.id &&
!user?.role?.includes("demo") &&
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
const handleDeleteUser = async () => {
const result = await deleteFn(`/auth/users/${userId}`);
setShowDeleteDialog(false);
if (result) {
navigate("/account", { state: { tab: "team" } });
}
};
const onSubmit = async (data: EditUserFormData) => {
await patch(`/auth/users/${userId}`, data);
};
@@ -165,17 +190,45 @@ const EditUserPage = () => {
</Stack>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
<Stack
gap={LAYOUT.XS}
direction="row"
justifyContent={"flex-end"}
width="100%"
>
{t("common.buttons.save")}
</Button>
{canDeleteUser && (
<Button
variant="contained"
color="error"
onClick={() => setShowDeleteDialog(true)}
sx={{ minWidth: 100 }}
>
{t("common.buttons.removeUser")}
</Button>
)}
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
</Stack>
</form>
<DialogInput
open={showDeleteDialog}
title={t("pages.editUser.dialog.removeUser.title")}
content={t("pages.editUser.dialog.removeUser.content", {
name: `${user?.firstName} ${user?.lastName}`,
})}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteUser}
confirmText={t("common.buttons.removeUser")}
loading={isDeleting}
/>
</BasePage>
);
};
@@ -43,6 +43,7 @@ export const AddTeamMemberDialog = ({
const roleOptions: { value: UserRole; label: string }[] = [
{ value: "admin", label: t("common.auth.roles.admin") },
{ value: "user", label: t("common.auth.roles.user") },
{ value: "demo", label: t("common.auth.roles.demo") },
];
const handleClose = () => {
@@ -48,6 +48,7 @@ export const InviteTeamMemberDialog = ({
const roleOptions: { value: UserRole; label: string }[] = [
{ value: "admin", label: t("common.auth.roles.admin") },
{ value: "user", label: t("common.auth.roles.user") },
{ value: "demo", label: t("common.auth.roles.demo") },
];
const handleGenerateToken = async (data: InviteFormData) => {
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Table } from "@/Components/design-elements";
import type { Header } from "@/Components/design-elements/Table";
import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import type { User } from "@/Types/User";
interface TeamTableProps {
@@ -13,7 +13,7 @@ interface TeamTableProps {
export const TeamTable = ({ users }: TeamTableProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const isSuperAdmin = useIsSuperAdmin();
const isAdmin = useIsAdmin();
const headers: Header<User>[] = [
{
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
];
const handleRowClick = (row: User) => {
if (isSuperAdmin) {
if (isAdmin) {
navigate(`/account/team/${row.id}`);
}
};
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
<Table
headers={headers}
data={users}
onRowClick={isSuperAdmin ? handleRowClick : undefined}
onRowClick={isAdmin ? handleRowClick : undefined}
/>
);
};
+15 -7
View File
@@ -1,6 +1,7 @@
import { BaseAuthPage, TextLink } from "@/Components/design-elements";
import { Button, TextField } from "@/Components/inputs";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
@@ -9,7 +10,7 @@ import type { LoginFormData } from "@/Validation/login";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setAuthState } from "@/Features/Auth/authSlice";
import { usePost } from "@/Hooks/UseApi";
import { useLazyGet, usePost } from "@/Hooks/UseApi";
const LoginPage = () => {
const { t } = useTranslation();
@@ -17,6 +18,17 @@ const LoginPage = () => {
const navigate = useNavigate();
const { post, loading } = usePost();
const [isCheckingAdmin, setIsCheckingAdmin] = useState(true);
const { get } = useLazyGet<boolean>();
useEffect(() => {
get("/auth/users/superadmin").then((res) => {
if (res?.data === false) navigate("/register", { replace: true });
else setIsCheckingAdmin(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { schema, defaults } = useLoginForm();
const { control, handleSubmit } = useForm<LoginFormData>({
@@ -24,6 +36,8 @@ const LoginPage = () => {
defaultValues: defaults,
});
if (isCheckingAdmin) return null;
const onSubmit = async (data: LoginFormData) => {
if (loading) return;
@@ -82,12 +96,6 @@ const LoginPage = () => {
linkText={t("pages.auth.login.links.forgotPassword.linkText")}
href="/forgot-password"
/>
<TextLink
alignSelf={"center"}
text={t("pages.auth.login.links.register.text")}
linkText={t("pages.auth.login.links.register.linkText")}
href="/register"
/>
</BaseAuthPage>
);
};
+15 -11
View File
@@ -1,16 +1,17 @@
import { BaseAuthPage } from "@/Components/design-elements";
import { Button, TextField } from "@/Components/inputs";
import Alert from "@mui/material/Alert";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
import { useRegisterForm } from "@/Hooks/useRegisterForm";
import type { RegisterFormData } from "@/Validation/register";
import { useTranslation } from "react-i18next";
import { usePost, useGet } from "@/Hooks/UseApi";
import { useLazyGet, usePost } from "@/Hooks/UseApi";
import { setAuthState } from "@/Features/Auth/authSlice";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import type { AuthResponse } from "@/Types/User";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
interface RegisterPayload {
user: Omit<RegisterFormData, "confirm">;
@@ -32,21 +33,23 @@ const RegisterPage = () => {
const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>();
const hasVerified = useRef(false);
const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet<boolean>(
token ? null : "/auth/users/superadmin"
);
const [isCheckingAdmin, setIsCheckingAdmin] = useState(!token);
const { get } = useLazyGet<boolean>();
useEffect(() => {
if (token) return;
get("/auth/users/superadmin").then((res) => {
if (res?.data === true) navigate("/login", { replace: true });
else setIsCheckingAdmin(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { control, handleSubmit, setError, reset } = useForm<RegisterFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
});
useEffect(() => {
if (superAdminExists === true) {
navigate("/login", { replace: true });
}
}, [superAdminExists, navigate]);
useEffect(() => {
if (!token || hasVerified.current) return;
hasVerified.current = true;
@@ -92,6 +95,7 @@ const RegisterPage = () => {
title={t("pages.auth.register.title")}
subtitle={t("pages.auth.register.subtitle")}
>
{!token && <Alert severity="info">{t("pages.auth.register.setupNotice")}</Alert>}
<Controller
name="firstName"
control={control}
+34 -9
View File
@@ -32,7 +32,12 @@ import {
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
import {
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
@@ -141,6 +146,17 @@ const getGeneralSettingsConfig = (
showGrpcServiceName: false,
showIgnoreTls: false,
},
websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: true,
},
};
return configs[type] || configs.http;
};
@@ -286,46 +302,53 @@ const CreateMonitorPage = () => {
>
<RadioWithDescription
value="http"
label={t("pages.createMonitor.form.type.optionHttp")}
label={t("pages.common.monitors.monitorTypes.optionHttp")}
description={t(
"pages.createMonitor.form.type.optionHttpDescription"
)}
/>
<RadioWithDescription
value="ping"
label={t("pages.createMonitor.form.type.optionPing")}
label={t("pages.common.monitors.monitorTypes.optionPing")}
description={t(
"pages.createMonitor.form.type.optionPingDescription"
)}
/>
<RadioWithDescription
value="docker"
label={t("pages.createMonitor.form.type.optionDocker")}
label={t("pages.common.monitors.monitorTypes.optionDocker")}
description={t(
"pages.createMonitor.form.type.optionDockerDescription"
)}
/>
<RadioWithDescription
value="port"
label={t("pages.createMonitor.form.type.optionPort")}
label={t("pages.common.monitors.monitorTypes.optionPort")}
description={t(
"pages.createMonitor.form.type.optionPortDescription"
)}
/>
<RadioWithDescription
value="game"
label={t("pages.createMonitor.form.type.optionGame")}
label={t("pages.common.monitors.monitorTypes.optionGame")}
description={t(
"pages.createMonitor.form.type.optionGameDescription"
)}
/>
<RadioWithDescription
value="grpc"
label={t("pages.createMonitor.form.type.optionGrpc")}
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
description={t(
"pages.createMonitor.form.type.optionGrpcDescription"
)}
/>
<RadioWithDescription
value="websocket"
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
description={t(
"pages.createMonitor.form.type.optionWebSocketDescription"
)}
/>
</RadioGroup>
</FormControl>
)}
@@ -742,7 +765,9 @@ const CreateMonitorPage = () => {
}
/>
{(watchedType === "http" || watchedType === "grpc") && (
{(watchedType === "http" ||
watchedType === "grpc" ||
watchedType === "websocket") && (
<ConfigBox
title={t("pages.createMonitor.form.ignoreTls.title")}
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
@@ -885,7 +910,7 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
{supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
+196 -22
View File
@@ -147,27 +147,201 @@ const NotificationsCreatePage = () => {
/>
}
/>
{watchedType !== "matrix" && (
{watchedType !== "matrix" &&
watchedType !== "telegram" &&
watchedType !== "pushover" &&
watchedType !== "twilio" && (
<ConfigBox
title={addressConfig.title}
subtitle={addressConfig.description}
rightContent={
<Controller
name="address"
control={control}
defaultValue={"address" in defaults ? defaults.address : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={addressConfig.fieldLabel}
placeholder={addressConfig.placeholder}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
}
/>
)}
{watchedType === "telegram" && (
<ConfigBox
title={addressConfig.title}
subtitle={addressConfig.description}
title={t("pages.notifications.form.telegram.title")}
subtitle={t("pages.notifications.form.telegram.description")}
rightContent={
<Controller
name="address"
control={control}
defaultValue={defaults.address}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={addressConfig.fieldLabel}
placeholder={addressConfig.placeholder}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Stack spacing={theme.spacing(8)}>
<Controller
name="accessToken"
control={control}
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.telegram.optionBotToken")}
placeholder={t(
"pages.notifications.form.telegram.placeholderBotToken"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="address"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.telegram.optionChatId")}
placeholder={t("pages.notifications.form.telegram.placeholderChatId")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "pushover" && (
<ConfigBox
title={t("pages.notifications.form.pushover.title")}
subtitle={t("pages.notifications.form.pushover.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="accessToken"
control={control}
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.pushover.optionAppToken")}
placeholder={t(
"pages.notifications.form.pushover.placeholderAppToken"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="address"
control={control}
defaultValue={"address" in defaults ? defaults.address : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.pushover.optionUserKey")}
placeholder={t(
"pages.notifications.form.pushover.placeholderUserKey"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "twilio" && (
<ConfigBox
title={t("pages.notifications.form.twilio.title")}
subtitle={t("pages.notifications.form.twilio.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="accountSid"
control={control}
defaultValue={"accountSid" in defaults ? defaults.accountSid : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.twilio.optionAccountSid")}
placeholder={t(
"pages.notifications.form.twilio.placeholderAccountSid"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="accessToken"
control={control}
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.twilio.optionAuthToken")}
placeholder={t(
"pages.notifications.form.twilio.placeholderAuthToken"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="twilioPhoneNumber"
control={control}
defaultValue={
"twilioPhoneNumber" in defaults ? defaults.twilioPhoneNumber : ""
}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.twilio.optionFromNumber")}
placeholder={t(
"pages.notifications.form.twilio.placeholderFromNumber"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="phone"
control={control}
defaultValue={"phone" in defaults ? defaults.phone : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.twilio.optionToNumber")}
placeholder={t("pages.notifications.form.twilio.placeholderToNumber")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
@@ -180,7 +354,7 @@ const NotificationsCreatePage = () => {
<Controller
name="homeserverUrl"
control={control}
defaultValue={defaults.homeserverUrl}
defaultValue={"homeserverUrl" in defaults ? defaults.homeserverUrl : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
@@ -196,7 +370,7 @@ const NotificationsCreatePage = () => {
<Controller
name="roomId"
control={control}
defaultValue={defaults.roomId}
defaultValue={"roomId" in defaults ? defaults.roomId : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
@@ -212,7 +386,7 @@ const NotificationsCreatePage = () => {
<Controller
name="accessToken"
control={control}
defaultValue={defaults.accessToken}
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
+41 -24
View File
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData } from "@/Validation/settings";
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings";
import { useState } from "react";
import { Controller } from "react-hook-form";
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
@@ -37,6 +37,7 @@ import {
} from "@/Features/UI/uiSlice.js";
import timezones from "@/Utils/timezones.json";
import type { RootState } from "@/Types/state";
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
interface Timezone {
id: string;
@@ -88,7 +89,7 @@ export const SettingsPage = () => {
// Initialize form with schema and defaults
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
const form = useForm<SettingsFormData>({
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
mode: "onChange",
@@ -255,28 +256,7 @@ export const SettingsPage = () => {
delete (dataToSend as any).systemEmailPassword;
}
// Convert undefined to empty string for backend unsetting
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
const typedKey = key as keyof SettingsFormData;
if (value === undefined) {
(acc as any)[typedKey] = "";
} else if (typeof value === "object" && value !== null) {
// Handle nested objects like globalThresholds
// Convert 0 to "" for unsetting thresholds
(acc as any)[typedKey] = Object.entries(value).reduce(
(nested, [nestedKey, nestedValue]) => ({
...nested,
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
}),
{}
);
} else {
(acc as any)[typedKey] = value;
}
return acc;
}, {} as Partial<SettingsFormData>);
const result = await patch("/settings", processedData as SettingsFormData);
const result = await patch("/settings", dataToSend as SettingsFormData);
if (result?.success) {
// Update API key state from response
@@ -462,6 +442,43 @@ export const SettingsPage = () => {
/>
)}
{/* Check Retention */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.retention.title")}
subtitle={t("pages.settings.form.retention.description")}
rightContent={
<Controller
name="checkTTL"
control={form.control}
defaultValue={defaults.checkTTL}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.retention.option.days.label")}
min={1}
max={CHECK_TTL_SENTINEL}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 30}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
valueLabelFormat={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
formatDisplayValue={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
/>
)}
/>
}
/>
)}
{/* Global Thresholds */}
{isAdmin && (
<ConfigBox
+58 -8
View File
@@ -21,19 +21,28 @@ import {
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
import type { StatusPageFormData } from "@/Validation/statusPage";
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import type { StatusPageResponse } from "@/Types/StatusPage";
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
import { getMonitorTypeLabel } from "@/Types/StatusPage";
import timezones from "@/Utils/timezones.json";
import { useNavigate, useParams } from "react-router-dom";
import axios from "axios";
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
const monitorsUrl = (() => {
const params = new URLSearchParams();
["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
(type) => params.append("type", type)
);
return `/monitors/team?${params.toString()}`;
})();
interface TimezoneOption {
_id: string;
name: string;
@@ -49,11 +58,11 @@ const CreateStatusPage = () => {
// Fetch existing status page data when configuring
const { data: statusPageData, isLoading: isLoadingStatusPage } =
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
useGet<StatusPageResponse>(
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
);
const { data: monitorsResponse } = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker"
);
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl);
const monitors = monitorsResponse ?? [];
const { post, loading: isSubmittingPost } = usePost();
@@ -81,6 +90,24 @@ const CreateStatusPage = () => {
reset(defaults);
}, [defaults, reset]);
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
const computedTypes: MonitorDisplayType[] = useMemo(() => {
const selectedMonitors = (watchedMonitorIds ?? [])
.map((id) => monitors.find((m) => m.id === id))
.filter((m): m is Monitor => m !== undefined);
const typesSet = new Set<MonitorDisplayType>();
selectedMonitors.forEach((m) => {
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
});
return typesSet.size ? Array.from(typesSet) : ["uptime"];
}, [watchedMonitorIds, monitors]);
useEffect(() => {
form.setValue("type", computedTypes);
}, [computedTypes]);
const onError = (errors: any) => {
logger.debug("Status page validation errors", errors);
};
@@ -103,7 +130,6 @@ const CreateStatusPage = () => {
const onSubmit = async (data: StatusPageFormData) => {
const fd = new FormData();
fd.append("type", "uptime");
fd.append("isPublished", String(data.isPublished));
if (data.companyName) fd.append("companyName", data.companyName);
if (data.url) fd.append("url", data.url);
@@ -112,11 +138,16 @@ const CreateStatusPage = () => {
fd.append("showCharts", String(data.showCharts));
fd.append("showUptimePercentage", String(data.showUptimePercentage));
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
fd.append("showInfrastructure", String(data.showInfrastructure));
data.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
});
data.type.forEach((type) => {
fd.append("type[]", type);
});
// Handle logo upload
if (data.logo === null) {
// Signal to remove the logo
@@ -303,7 +334,9 @@ const CreateStatusPage = () => {
}}
>
<GripVertical size={20} />
<Typography flexGrow={1}>{monitor.name}</Typography>
<Typography
flexGrow={1}
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
<IconButton
size="small"
onClick={() => {
@@ -425,6 +458,23 @@ const CreateStatusPage = () => {
/>
)}
/>
<Controller
name="showInfrastructure"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
}
label={t(
"pages.statusPages.form.features.option.showInfrastructure.label"
)}
/>
)}
/>
{/* <Controller
name="showUptimePercentage"
control={control}
@@ -0,0 +1,246 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Gauge } from "@/Components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import prettyBytes from "pretty-bytes";
import type { Monitor } from "@/Types/Monitor";
import Grid from "@mui/material/Grid";
import useMediaQuery from "@mui/material/useMediaQuery";
import Box from "@mui/material/Box";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
const GAUGE_RADIUS = 60;
const GAUGE_STROKE_WIDTH = 12;
const PERCENTAGE_MULTIPLIER = 100;
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
}
interface MetricDetail {
label: string;
value: string;
}
interface MetricConfig {
key: string;
label: string;
hasData: boolean;
progress: number;
details: MetricDetail[];
}
const MetricDetailRow = ({ label, value }: MetricDetail) => {
const theme = useTheme();
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{label}
</Typography>
<Typography variant="body2">{value}</Typography>
</Stack>
);
};
interface MetricItemProps {
label: string;
progress: number;
details?: MetricDetail[];
}
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Grid
size={isSmall ? 12 : 4}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: theme.spacing(SPACING.LG),
padding: theme.spacing(LAYOUT.XS),
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
"&:last-child": {
borderRight: "none",
borderBottom: "none",
paddingBottom: theme.spacing(SPACING.LG),
},
}}
>
<Box
display="flex"
flexDirection="column"
alignItems="center"
>
<Gauge
progress={progress}
radius={GAUGE_RADIUS}
strokeWidth={GAUGE_STROKE_WIDTH}
/>
<Typography variant="body2">{label}</Typography>
</Box>
{details && details.length > 0 && (
<Box
width="100%"
paddingX={theme.spacing(LAYOUT.LG)}
>
{details.map((detail) => (
<MetricDetailRow
key={detail.label}
label={detail.label}
value={detail.value}
/>
))}
</Box>
)}
</Grid>
);
};
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
const buildCpuMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
return null;
}
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
return {
key: "cpu",
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
hasData: true,
progress: usagePercent,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
value: `${usagePercent.toFixed(2)}%`,
},
],
};
};
const buildMemoryMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (
!check.memory ||
typeof check.memory.usage_percent !== "number" ||
typeof check.memory.used_bytes !== "number" ||
typeof check.memory.total_bytes !== "number"
) {
return null;
}
return {
key: "memory",
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
hasData: true,
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.used"),
value: prettyBytes(check.memory.used_bytes ?? 0),
},
{
label: t("pages.statusPages.monitorsList.infrastructure.total"),
value: prettyBytes(check.memory.total_bytes ?? 0),
},
],
};
};
const buildDiskMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (!check.disk || check.disk.length === 0) {
return null;
}
const disks = check.disk;
const avgUsagePercent =
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
PERCENTAGE_MULTIPLIER;
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
return {
key: "disk",
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
hasData: true,
progress: avgUsagePercent,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.used"),
value: prettyBytes(totalUsedBytes),
},
{
label: t("pages.statusPages.monitorsList.infrastructure.total"),
value: prettyBytes(totalTotalBytes),
},
],
};
};
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
const theme = useTheme();
const { t } = useTranslation();
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
if (!latestCheck) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
const metrics: MetricConfig[] = [
buildCpuMetric(latestCheck, t),
buildMemoryMetric(latestCheck, t),
buildDiskMetric(latestCheck, t),
].filter((m): m is MetricConfig => m !== null);
if (metrics.length === 0) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
return (
<Grid
container
alignItems="center"
padding={theme.spacing(LAYOUT.XS)}
>
{metrics.map(({ key, label, progress, details }) => (
<MetricItem
key={key}
label={label}
progress={progress}
details={details}
/>
))}
</Grid>
);
};
@@ -5,13 +5,16 @@ import Typography from "@mui/material/Typography";
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
import { StatusLabel, BaseBox } from "@/Components/design-elements";
import { SwitchComponent } from "@/Components/inputs";
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
import { useTheme } from "@mui/material/styles";
import { useTheme, type Theme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { useState } from "react";
import type { Monitor } from "@/Types/Monitor";
import type { StatusPage } from "@/Types/StatusPage";
import { getMonitorTypeLabel } from "@/Types/StatusPage";
import type { RootState } from "@/Types/state";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
@@ -22,29 +25,124 @@ interface MonitorsListProps {
monitors: StatusPageMonitor[];
}
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
const bg =
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
return {
backgroundColor: bg,
color: theme.palette.background.paper,
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
borderRadius: theme.shape.borderRadius,
};
};
const MonitorHeader = ({
monitor,
showURL,
}: {
monitor: StatusPageMonitor;
showURL: boolean;
}) => {
const theme = useTheme();
const { t } = useTranslation();
const showURL = useSelector((state: RootState) => state.ui?.showURL);
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(LAYOUT.XS)}
mb={theme.spacing(LAYOUT.XS)}
>
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(SPACING.LG)}
mb={theme.spacing(SPACING.SM)}
>
<Typography
variant="h6"
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.name}
</Typography>
<Typography
variant="caption"
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
>
{getMonitorTypeLabel(monitor.type, t)}
</Typography>
</Stack>
{showURL && monitor.url && (
<Typography
variant="body2"
color={theme.palette.text.secondary}
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel status={monitor.status} />
</Stack>
);
};
const MonitorContent = ({
monitor,
statusPage,
chartType,
}: {
monitor: StatusPageMonitor;
statusPage: StatusPage;
chartType: string;
}) => {
const theme = useTheme();
if (monitor.type === "hardware") {
if (statusPage.showInfrastructure === false) return null;
return <InfrastructureMetrics monitor={monitor} />;
}
if (statusPage.showCharts === false) return null;
const checks = monitor.checks?.slice().reverse() ?? [];
return (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
checks={checks}
/>
) : (
<HeatmapResponseTime checks={checks} />
)}
</Box>
);
};
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const theme = useTheme();
const { t } = useTranslation();
const showURL = useSelector((state: RootState) => state.ui?.showURL);
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
return (
<Stack gap={theme.spacing(8)}>
{statusPage.showCharts !== false && (
<Stack gap={theme.spacing(LAYOUT.MD)}>
{statusPage.showCharts && (
<Stack
direction={"row"}
alignItems={"center"}
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.SM)}
>
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
<SwitchComponent
dualOption
value={chartType}
checked={chartType === "histogram"}
onChange={(e) => {
setChartType(e.target.checked ? "histogram" : "heatmap");
}}
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")}
/>
<Typography>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
@@ -52,59 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
</Stack>
)}
{monitors?.map((monitor) => {
return (
<BaseBox
key={monitor.id}
padding={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(4)}
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
>
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
<Typography
variant="h6"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{monitor.name}
</Typography>
{showURL && (
<Typography
variant="body2"
color="text.secondary"
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel status={monitor.status} />
</Stack>
{statusPage.showCharts !== false && (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
<HeatmapResponseTime
checks={monitor?.checks?.slice().reverse() ?? []}
/>
)}
</Box>
)}
</BaseBox>
);
})}
{monitors.map((monitor) => (
<BaseBox
key={monitor.id}
padding={theme.spacing(LAYOUT.MD)}
>
<MonitorHeader
monitor={monitor}
showURL={showURL}
/>
<MonitorContent
monitor={monitor}
statusPage={statusPage}
chartType={chartType}
/>
</BaseBox>
))}
</Stack>
);
};
@@ -1,35 +1,115 @@
import { AlertTriangle, CircleCheck } from "lucide-react";
import {
AlertTriangle,
CircleCheck,
CircleX,
Loader,
PauseCircle,
ShieldAlert,
Wrench,
} from "lucide-react";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import type { Theme } from "@mui/material";
import { useTheme } from "@mui/material";
import type { Monitor } from "@/Types/Monitor";
import type { Theme } from "@mui/material";
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
interface StatusDisplay {
icon: JSX.Element;
msg?: string;
color?: string;
}
const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
const monitorsStatus: Record<string, any> = {
const monitorsStatus: StatusDisplay = {
icon: <AlertTriangle size={24} />,
};
if (monitors.every((monitor) => monitor.status === "up")) {
// Handle empty monitors array
if (monitors.length === 0) {
monitorsStatus.msg = t("pages.statusPages.statusBar.noMonitors");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <CircleX size={24} />;
return monitorsStatus;
}
const allOf = (...statuses: MonitorStatus[]) =>
monitors.every((m) => statuses.includes(m.status));
const someOf = (...statuses: MonitorStatus[]) =>
monitors.some((m) => statuses.includes(m.status));
const noneOf = (...statuses: MonitorStatus[]) =>
monitors.every((m) => !statuses.includes(m.status));
// All monitors in a single state
if (allOf("up")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
monitorsStatus.color = theme.palette.success.main;
monitorsStatus.icon = <CircleCheck size={24} />;
return monitorsStatus;
} else if (monitors.every((monitor) => monitor.status === "down")) {
} else if (allOf("breached")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allBreached");
monitorsStatus.color = theme.palette.error.main;
monitorsStatus.icon = <ShieldAlert size={24} />;
} else if (allOf("maintenance")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allMaintenance");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <Wrench size={24} />;
} else if (allOf("down")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
monitorsStatus.color = theme.palette.error.main;
return monitorsStatus;
} else if (monitors.some((monitor) => monitor.status === "down")) {
monitorsStatus.icon = <CircleX size={24} />;
} else if (allOf("paused")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allPaused");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <PauseCircle size={24} />;
} else if (allOf("initializing")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allInitializing");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <Loader size={24} />;
// Breached takes highest priority in mixed states
} else if (someOf("breached") && someOf("down")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.breachedAndDown");
monitorsStatus.color = theme.palette.error.main;
monitorsStatus.icon = <ShieldAlert size={24} />;
} else if (someOf("breached")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.breached");
monitorsStatus.color = theme.palette.error.main;
monitorsStatus.icon = <ShieldAlert size={24} />;
// Maintenance combinations
} else if (someOf("maintenance") && someOf("down")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenanceAndDown");
monitorsStatus.color = theme.palette.error.main;
monitorsStatus.icon = <Wrench size={24} />;
} else if (someOf("maintenance") && noneOf("down")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenance");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <Wrench size={24} />;
// Degraded (some down, no maintenance/breached)
} else if (someOf("down")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
monitorsStatus.color = theme.palette.warning.main;
return monitorsStatus;
monitorsStatus.icon = <AlertTriangle size={24} />;
// Some Paused
} else if (someOf("paused")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.partiallyPaused");
monitorsStatus.color = theme.palette.warning.main;
monitorsStatus.icon = <PauseCircle size={24} />;
// Initializing
} else if (someOf("initializing")) {
monitorsStatus.msg = t("pages.statusPages.statusBar.initializing");
monitorsStatus.color = theme.palette.info.main;
monitorsStatus.icon = <Loader size={24} />;
} else {
monitorsStatus.msg = t("pages.statusPages.statusBar.unknown");
monitorsStatus.color = theme.palette.warning.main;
return monitorsStatus;
}
return monitorsStatus;
};
interface StatusBarProps {
+9 -3
View File
@@ -23,16 +23,22 @@ const StatusPageView = () => {
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isPublic = location.pathname.startsWith("/status/public");
const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
const { data, isLoading, error } = useGet<StatusPageResponse>(
apiUrl,
{},
{
refreshInterval: 10000,
}
);
const statusPage = data?.statusPage;
const monitors = data?.monitors ?? [];
if (!statusPage) return null;
if (monitors?.length === 0) {
if (monitors.length === 0) {
return (
<BasePage
loading={isLoading}
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
{
id: "type",
content: t("common.table.headers.type"),
render: (row) => row.type,
render: (row) => row.type.join(", "),
},
{
id: "status",
+3 -3
View File
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type {
GeoChecksResult,
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
);
const geoChecksUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
// Fetch paginated geo checks for the table
const geoChecksTableUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
+1 -1
View File
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
const effectiveTypes =
selectedTypes.length > 0
? selectedTypes
: ["http", "ping", "docker", "port", "game", "grpc"];
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
// Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => {
+3 -8
View File
@@ -1,3 +1,5 @@
export const CHECK_TTL_SENTINEL = 366;
export interface CheckMetadata {
monitorId: string;
teamId: string;
@@ -125,9 +127,6 @@ export interface Check {
timings?: CheckTimings;
statusCode: number;
message: string;
ack: boolean;
ackAt?: string | null;
expiry: string;
cpu?: CheckCpuInfo;
memory?: CheckMemoryInfo;
disk?: CheckDiskInfo[];
@@ -140,7 +139,6 @@ export interface Check {
seo?: number;
performance?: number;
audits?: CheckAudits;
__v: number;
createdAt: string;
updatedAt: string;
}
@@ -243,10 +241,7 @@ export interface ChecksSummary {
downChecks: number;
}
export type CheckSnapshot = Omit<
Check,
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
> & {
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & {
originalResponseTime: number;
};
+10
View File
@@ -12,10 +12,20 @@ export const MonitorTypes = [
"port",
"game",
"grpc",
"websocket",
"unknown",
] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
if (!type) {
return false;
}
return GeoCheckSupportedTypes.includes(type);
};
export const MonitorStatuses = [
"up",
"down",
+6
View File
@@ -5,6 +5,10 @@ export const NotificationChannels = [
"webhook",
"pager_duty",
"matrix",
"teams",
"telegram",
"pushover",
"twilio",
] as const;
export type NotificationChannel = (typeof NotificationChannels)[number];
@@ -19,6 +23,8 @@ export interface Notification {
homeserverUrl?: string;
roomId?: string;
accessToken?: string;
accountSid?: string;
twilioPhoneNumber?: string;
createdAt: string;
updatedAt: string;
}
+24 -2
View File
@@ -1,10 +1,31 @@
import type { Monitor } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
export type MonitorDisplayType = "uptime" | "infrastructure";
export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
http: "pages.common.monitors.monitorTypes.optionHttp",
ping: "pages.common.monitors.monitorTypes.optionPing",
docker: "pages.common.monitors.monitorTypes.optionDocker",
port: "pages.common.monitors.monitorTypes.optionPort",
game: "pages.common.monitors.monitorTypes.optionGame",
grpc: "pages.common.monitors.monitorTypes.optionGrpc",
websocket: "pages.common.monitors.monitorTypes.optionWebSocket",
hardware: "pages.common.monitors.monitorTypes.optionHardware",
pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed",
};
export const getMonitorTypeLabel = (
type: MonitorType,
t: (key: string) => string
): string => {
const key = MONITOR_TYPE_KEYS[type];
return key ? t(key) : type;
};
export interface StatusPage {
id: string;
userId: string;
teamId: string;
type: string;
type: MonitorDisplayType[];
companyName: string;
url: string;
timezone?: string;
@@ -20,6 +41,7 @@ export interface StatusPage {
showCharts: boolean;
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
showInfrastructure: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
+1
View File
@@ -9,6 +9,7 @@ export const getMonitorPath = (type: MonitorType): string => {
ping: "uptime",
game: "uptime",
grpc: "uptime",
websocket: "uptime",
unknown: "uptime",
docker: "uptime",
hardware: "infrastructure",
+6
View File
@@ -12,6 +12,8 @@ export const typographyLevels = {
export const colors = {
gray200: "#EFEFEF",
gray700: "#313131",
gray900: "#151518",
gray850: "#1c1c21",
blueBlueWave: "#1570EF",
};
@@ -31,4 +33,8 @@ export const darkPalette = {
secondary: {
main: colors.gray700,
},
background: {
default: colors.gray900,
paper: colors.gray850,
},
};
+9
View File
@@ -115,6 +115,13 @@ const hardwareSchema = baseSchema.extend({
selectedDisks: z.array(z.string()),
});
// WebSocket monitor schema
const websocketSchema = baseSchema.extend({
type: z.literal("websocket"),
url: z.string().min(1, "WebSocket URL is required"),
ignoreTlsErrors: z.boolean(),
});
// Discriminated union of all monitor types
export const monitorSchema = z.discriminatedUnion("type", [
httpSchema,
@@ -125,6 +132,7 @@ export const monitorSchema = z.discriminatedUnion("type", [
grpcSchema,
pagespeedSchema,
hardwareSchema,
websocketSchema,
]);
export type MonitorFormData = z.infer<typeof monitorSchema>;
@@ -139,4 +147,5 @@ export {
grpcSchema,
pagespeedSchema,
hardwareSchema,
websocketSchema,
};
+29
View File
@@ -45,6 +45,31 @@ const matrixSchema = baseSchema.extend({
accessToken: z.string().min(1, "Access token is required"),
});
const teamsSchema = baseSchema.extend({
type: z.literal("teams"),
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
});
const telegramSchema = baseSchema.extend({
type: z.literal("telegram"),
address: z.string().min(1, "Chat ID is required"),
accessToken: z.string().min(1, "Bot token is required"),
});
const pushoverSchema = baseSchema.extend({
type: z.literal("pushover"),
address: z.string().min(1, "User key is required"),
accessToken: z.string().min(1, "App token is required"),
});
const twilioSchema = baseSchema.extend({
type: z.literal("twilio"),
accountSid: z.string().min(1, "Account SID is required"),
accessToken: z.string().min(1, "Auth token is required"),
phone: z.string().min(1, "Recipient phone number is required"),
twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"),
});
export const notificationSchema = z.discriminatedUnion("type", [
emailSchema,
slackSchema,
@@ -52,6 +77,10 @@ export const notificationSchema = z.discriminatedUnion("type", [
webhookSchema,
pagerDutySchema,
matrixSchema,
teamsSchema,
telegramSchema,
pushoverSchema,
twilioSchema,
]);
export type NotificationFormData = z.infer<typeof notificationSchema>;
+14 -9
View File
@@ -1,3 +1,4 @@
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
import { z } from "zod";
export const settingsSchema = z.object({
@@ -6,39 +7,42 @@ export const settingsSchema = z.object({
systemEmailRejectUnauthorized: z.boolean(),
systemEmailConnectionHost: z
.string()
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
showURL: z.boolean().optional(),
checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
checkTTL: z
.number()
.int()
.min(1, "Please enter a value")
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
pagespeedApiKey: z
.string()
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
systemEmailHost: z
.string()
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
systemEmailPort: z.number().int().min(1).max(65535).optional(),
systemEmailAddress: z
.string()
.email("Please enter a valid email address")
.or(z.literal(""))
.transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
.transform((val) => (val === "" ? null : val.toLowerCase().trim()))
.optional(),
systemEmailUser: z
.string()
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
systemEmailPassword: z
.string()
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
systemEmailTLSServername: z
.string()
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
globalThresholds: z.object({
cpu: z.number().int().min(1).max(100),
@@ -48,4 +52,5 @@ export const settingsSchema = z.object({
}),
});
export type SettingsFormInput = z.input<typeof settingsSchema>;
export type SettingsFormData = z.infer<typeof settingsSchema>;
+4
View File
@@ -14,12 +14,16 @@ export const statusPageSchema = z.object({
"URL can only contain lowercase letters, numbers, and hyphens"
),
timezone: z.string().optional(),
type: z
.array(z.enum(["uptime", "infrastructure"]))
.min(1, "At least one type is required"),
color: z.string().min(1, "Color is required"),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
isPublished: z.boolean(),
showCharts: z.boolean(),
showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(),
showInfrastructure: z.boolean(),
customCSS: z.string().optional(),
logo: z
.object({
+8 -1
View File
@@ -288,7 +288,13 @@ const logoImageValidation = joi
.optional(); // Make entire object optional
const statusPageValidation = joi.object({
type: joi.string().valid("uptime").required(),
type: joi
.alternatives()
.try(
joi.string().valid("uptime", "infrastructure"),
joi.array().items(joi.string().valid("uptime", "infrastructure"))
)
.required(),
isPublished: joi.bool(),
companyName: joi
.string()
@@ -317,6 +323,7 @@ const statusPageValidation = joi.object({
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
showInfrastructure: joi.boolean(),
});
const settingsValidation = joi.object({
+1281 -1084
View File
File diff suppressed because it is too large. Load diff
+1281 -1084
View File
File diff suppressed because it is too large. Load diff
+1271 -1106
View File
File diff suppressed because it is too large. Load diff
+99 -12
View File
@@ -46,7 +46,8 @@
"sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON",
"importFromJSON": "Import from JSON",
"clearFilters": "Clear filters"
"clearFilters": "Clear filters",
"removeUser": "Remove user"
},
"charts": {
"labels": {
@@ -342,6 +343,7 @@
"register": {
"title": "Welcome back to Checkmate!",
"subtitle": "Sign up to get started",
"setupNotice": "This page is only available during initial setup. Once a super admin is registered, additional users can be invited from team settings.",
"submit": "Register"
},
"forgotPassword": {
@@ -396,16 +398,28 @@
"activeFor": "Active for",
"certificateExpiry": "Certificate expiry",
"lastCheck": "Last check",
"lastResponseTime": "Last response time"
"lastResponseTime": "Last response time",
"serviceIsDown": "Service is down"
},
"status": {
"down": "down",
"breached": "breached",
"breached": "threshold exceeded",
"initializing": "initializing",
"maintenance": "maintenance",
"paused": "paused",
"total": "total",
"up": "up"
},
"monitorTypes": {
"optionDocker": "Docker",
"optionGame": "Game",
"optionHttp": "HTTP(S)",
"optionPing": "Ping",
"optionPort": "Port",
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastructure",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
}
}
},
@@ -487,6 +501,10 @@
"grpcServiceName": {
"label": "Service name",
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
},
"wsUrl": {
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
}
},
"title": "General settings",
@@ -498,6 +516,7 @@
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
}
},
@@ -528,17 +547,12 @@
},
"type": {
"description": "Select the type of check to perform",
"optionDocker": "Docker",
"optionDockerDescription": "Use Docker to monitor if a container is running.",
"optionGame": "Game",
"optionGameDescription": "Monitor if a specific game server is online.",
"optionGrpc": "gRPC",
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
"optionHttp": "HTTP(S)",
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
"optionPing": "Ping",
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
"optionPort": "Port",
"optionPortDescription": "Monitor if a specific port on a server is open.",
"title": "Type"
},
@@ -619,6 +633,12 @@
"title": "Roles",
"description": "Assign roles to the user. Multiple roles can be selected."
}
},
"dialog": {
"removeUser": {
"title": "Remove user",
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
}
}
},
"incidents": {
@@ -933,6 +953,34 @@
"description": "Select the type of notification channel to create.",
"optionType": "Type",
"title": "Channel type"
},
"telegram": {
"title": "Telegram configuration",
"description": "Configure your Telegram bot to send notifications.",
"optionBotToken": "Bot token",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "Chat ID",
"placeholderChatId": "-1001234567890"
},
"pushover": {
"title": "Pushover configuration",
"description": "Configure Pushover to receive push notifications on your devices.",
"optionAppToken": "Application API token",
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
"optionUserKey": "User key",
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
},
"twilio": {
"title": "Twilio SMS configuration",
"description": "Configure Twilio to send SMS notifications to a phone number.",
"optionAccountSid": "Account SID",
"placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"optionAuthToken": "Auth token",
"placeholderAuthToken": "your_auth_token",
"optionFromNumber": "From number (Twilio)",
"placeholderFromNumber": "+15551234567",
"optionToNumber": "To number (recipient)",
"placeholderToNumber": "+15559876543"
}
},
"table": {
@@ -1027,7 +1075,7 @@
},
"stats": {
"title": "Monitor history",
"description": "Define how long you want to retain historical data. You can also clear all existing data.",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
"option": {
"clear": {
"label": "Clear all stats. This is irreversible."
@@ -1038,6 +1086,16 @@
"description": "This cannot be undone"
}
},
"retention": {
"title": "Check retention",
"description": "Set how long check data is retained before being automatically cleaned up.",
"option": {
"days": {
"label": "Retention period (days)",
"unlimited": "Unlimited"
}
}
},
"thresholds": {
"title": "Global Thresholds",
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
@@ -1154,12 +1212,38 @@
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",
"chartTypeHistogram": "Histogram"
"chartTypeHistogram": "Histogram",
"noData": "No data available",
"infrastructure": {
"title": "Infrastructure",
"cpu": "CPU",
"memory": "RAM",
"memoryText": "Memory",
"disk": "Disk",
"usage": "Usage",
"used": "Used",
"total": "Total"
},
"uptime": {
"title": "Uptime",
"responseTime": "Response time"
}
},
"statusBar": {
"allDown": "All systems are down",
"allUp": "All systems operational",
"allBreached": "All systems have exceeded thresholds",
"allMaintenance": "All systems are under maintenance",
"allDown": "All systems are down",
"allPaused": "All systems are paused",
"allInitializing": "All systems are initializing",
"breachedAndDown": "Some systems have exceeded thresholds and are down",
"breached": "Some systems have exceeded thresholds",
"degraded": "Some systems are experiencing issues",
"maintenanceAndDown": "Some systems are down during maintenance",
"maintenance": "Some systems are under maintenance",
"partiallyPaused": "Some systems are paused",
"initializing": "Some systems are initializing",
"noMonitors": "No monitors have been configured",
"unknown": "Unable to determine system status"
},
"table": {
@@ -1246,6 +1330,9 @@
},
"showAdminLoginLink": {
"label": "Show admin login link"
},
"showInfrastructure": {
"label": "Show infrastructure metrics"
}
}
}
+1281 -1084
View File
File diff suppressed because it is too large. Load diff
+1281 -1084
View File
File diff suppressed because it is too large. Load diff
+1271 -1106
View File
File diff suppressed because it is too large. Load diff
+1271 -1085
View File
File diff suppressed because it is too large. Load diff
+1271 -1106
View File
File diff suppressed because it is too large. Load diff
+1271 -1106
View File
File diff suppressed because it is too large. Load diff
+1271 -1106
View File
File diff suppressed because it is too large. Load diff
+1281 -1084
View File
File diff suppressed because it is too large. Load diff
+1268 -801
View File
File diff suppressed because it is too large. Load diff
+1268 -801
View File
File diff suppressed because it is too large. Load diff
+1271 -1098
View File
File diff suppressed because it is too large. Load diff
+1299 -1088
View File
File diff suppressed because it is too large. Load diff
+10 -1
View File
@@ -7,13 +7,22 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.5.0";
let version = "3.6.1";
const allowedHosts = process.env.VITE_ALLOWED_HOSTS
? process.env.VITE_ALLOWED_HOSTS.split(",").map((h) => h.trim())
: true;
return {
base: "/",
plugins: [svgr(), react()],
server: {
host: true,
allowedHosts,
},
preview: {
host: true,
allowedHosts,
},
resolve: {
alias: {
+36
View File
@@ -0,0 +1,36 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=bluewaveuptime/uptime_client
set dockerfiles[0]=.\docker\coolify\client.Dockerfile
set services[1]=bluewaveuptime/uptime_database_mongo
set dockerfiles[1]=.\docker\coolify\mongoDB.Dockerfile
set services[2]=bluewaveuptime/uptime_redis
set dockerfiles[2]=.\docker\coolify\redis.Dockerfile
set services[3]=bluewaveuptime/uptime_server
set dockerfiles[3]=.\docker\coolify\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2,3) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+5 -5
View File
@@ -1,14 +1,14 @@
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
cd "$(dirname "$0")"
cd ../../../
cd ../../
# Define an array of services and their Dockerfiles
declare -A services=(
["bluewaveuptime/uptime_client"]="./server/docker/dist/client.Dockerfile"
["bluewaveuptime/uptime_database_mongo"]="./server/docker/dist/mongoDB.Dockerfile"
["bluewaveuptime/uptime_redis"]="./server/docker/dist/redis.Dockerfile"
["bluewaveuptime/uptime_server"]="./server/docker/dist/server.Dockerfile"
["bluewaveuptime/uptime_client"]="./docker/coolify/client.Dockerfile"
["bluewaveuptime/uptime_database_mongo"]="./docker/coolify/mongoDB.Dockerfile"
["bluewaveuptime/uptime_redis"]="./docker/coolify/redis.Dockerfile"
["bluewaveuptime/uptime_server"]="./docker/coolify/server.Dockerfile"
)
# Loop through each service and build the corresponding image
+33
View File
@@ -0,0 +1,33 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=uptime_client
set dockerfiles[0]=.\docker\dev\client.Dockerfile
set services[1]=uptime_mongo
set dockerfiles[1]=.\docker\dev\mongoDB.Dockerfile
set services[2]=uptime_server
set dockerfiles[2]=.\docker\dev\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+6 -7
View File
@@ -1,17 +1,16 @@
# ---------------------
# Frontend build stage
# ---------------------
FROM node:20-slim AS frontend-build
# Build on the host arch (BUILDPLATFORM) rather than under QEMU — the output is
# pure JS so it's arch-independent, and this avoids the esbuild host/native
# binary version mismatch under emulation.
FROM --platform=$BUILDPLATFORM node:20-slim AS frontend-build
WORKDIR /app/client
COPY client/package.json ./
COPY client/package*.json ./
RUN npm install
RUN npm install esbuild@0.25.5 --build-from-source
RUN npm ci
COPY client ./
+30
View File
@@ -0,0 +1,30 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=uptime_database_mongo
set dockerfiles[0]=.\docker\dist-mono\mongoDB.Dockerfile
set services[1]=uptime_server
set dockerfiles[1]=.\docker\dist-mono\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+2 -2
View File
@@ -19,10 +19,10 @@ WORKDIR /app/server
COPY server ./
RUN npm install
RUN npm run build
RUN cp -r src/templates dist/templates
COPY --from=frontend-build /app/client/dist ./public
@@ -30,4 +30,4 @@ RUN chmod +x ./scripts/inject-vars.sh
EXPOSE 52345
CMD ./scripts/inject-vars.sh && node ./dist/index.js
CMD ./scripts/inject-vars.sh && node ./dist/index.js
+33
View File
@@ -0,0 +1,33 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=bluewaveuptime/uptime_client
set dockerfiles[0]=.\docker\dist\client.Dockerfile
set services[1]=bluewaveuptime/uptime_database_mongo
set dockerfiles[1]=.\docker\dist\mongoDB.Dockerfile
set services[2]=bluewaveuptime/uptime_server
set dockerfiles[2]=.\docker\dist\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+33
View File
@@ -0,0 +1,33 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=uptime_client
set dockerfiles[0]=.\docker\prod\client.Dockerfile
set services[1]=uptime_database_mongo
set dockerfiles[1]=.\docker\prod\mongoDB.Dockerfile
set services[2]=uptime_server
set dockerfiles[2]=.\docker\prod\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+3 -12
View File
@@ -3,8 +3,8 @@ services:
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1"
UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so"
ports:
- "80:80"
- "443:443"
@@ -12,16 +12,7 @@ services:
- server
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
- ./certbot/www:/var/www/certbot/:ro
- ./certbot/conf/:/etc/nginx/ssl/:ro
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
- /etc/letsencrypt:/etc/nginx/ssl:ro
server:
image: ghcr.io/bluewave-labs/checkmate:backend-demo
+2 -2
View File
@@ -40,8 +40,8 @@ server {
server_name checkmate-demo.bluewavelabs.ca;
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem;
location / {
root /usr/share/nginx/html;
+2
View File
@@ -17,6 +17,8 @@ COPY ./server ./
RUN npm run build
RUN cp -r src/templates dist/templates
EXPOSE 52345
CMD ["node", "dist/index.js"]
+33
View File
@@ -0,0 +1,33 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=uptime_client
set dockerfiles[0]=.\docker\staging\client.Dockerfile
set services[1]=uptime_database_mongo
set dockerfiles[1]=.\docker\staging\mongoDB.Dockerfile
set services[2]=uptime_server
set dockerfiles[2]=.\docker\staging\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+2
View File
@@ -17,6 +17,8 @@ COPY ./server ./
RUN npm run build
RUN cp -r src/templates dist/templates
EXPOSE 52345
CMD ["node", "dist/index.js"]
+33
View File
@@ -0,0 +1,33 @@
@echo off
setlocal enabledelayedexpansion
REM Change directory to root Server directory for correct Docker Context
cd /d "%~dp0"
cd ../..
REM Define service names and their corresponding Dockerfiles
set services[0]=uptime_client
set dockerfiles[0]=.\docker\unified\client.Dockerfile
set services[1]=uptime_database_mongo
set dockerfiles[1]=.\docker\unified\mongoDB.Dockerfile
set services[2]=uptime_server
set dockerfiles[2]=.\docker\unified\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
docker build -f "!dockerfile!" -t "!service!" .
REM Check if the build succeeded
if errorlevel 1 (
echo Error building !service! image. Exiting...
exit /b 1
)
)
echo All images built successfully
exit /b 0
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Change directory to root directory for correct Docker Context
cd "$(dirname "$0")"
cd ../../
# Define an array of services and their Dockerfiles
declare -A services=(
["uptime_client"]="./docker/unified/client.Dockerfile"
["uptime_database_mongo"]="./docker/unified/mongoDB.Dockerfile"
["uptime_server"]="./docker/unified/server.Dockerfile"
)
# Loop through each service and build the corresponding image
for service in "${!services[@]}"; do
docker build -f "${services[$service]}" -t "$service" .
# Check if the build succeeded
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
exit 1
fi
done
echo "All images built successfully"
+19
View File
@@ -0,0 +1,19 @@
version: "3"
services:
webserver:
image: nginx:latest
ports:
- 80:80
- 443:443
restart: always
volumes:
- ./nginx/conf.d/:/etc/nginx/conf.d/:ro
- ./certbot/www/:/var/www/certbot/:ro
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
depends_on:
- webserver
+20
View File
@@ -0,0 +1,20 @@
FROM node:20-slim AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
COPY ./client/package*.json ./
RUN npm install
COPY ./client .
RUN npm run build
FROM nginx:1.27.1-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
+114
View File
@@ -0,0 +1,114 @@
# sudo certbot certonly --standalone --http-01-port 8080
# cerbot needs to run on 8080 since port 80 is used by the gateway
services:
# Gateway (Single Entry Point)
gateway:
image: nginx:1.27.1-alpine
container_name: checkmate-gateway
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /etc/letsencrypt:/etc/nginx/ssl:ro
- ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# Production Stack -----
client-prod:
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
container_name: client-prod
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1"
UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so"
expose:
- "80"
depends_on:
- server-prod
volumes:
- ./nginx/conf.d/prod.conf:/etc/nginx/conf.d/default.conf:ro
server-prod:
image: ghcr.io/bluewave-labs/checkmate:backend-demo
container_name: server-prod
restart: always
env_file:
- server-prod.env
depends_on:
- mongodb-prod
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
mongodb-prod:
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
container_name: mongodb-prod
restart: always
command: [ "mongod", "--quiet", "--bind_ip_all" ]
volumes:
- ./mongo/data-prod:/data/db
env_file:
- mongo-prod.env
healthcheck:
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ]
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
# Staging Stack -----
client-staging:
image: ghcr.io/bluewave-labs/checkmate:frontend-staging
container_name: client-staging
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://staging.checkmate.so/api/v1"
UPTIME_APP_CLIENT_HOST: "https://staging.checkmate.so"
expose:
- "80"
depends_on:
- server-staging
volumes:
- ./nginx/conf.d/staging.conf:/etc/nginx/conf.d/default.conf:ro
server-staging:
image: ghcr.io/bluewave-labs/checkmate:backend-staging
container_name: server-staging
restart: always
env_file:
- server-staging.env
depends_on:
- mongodb-staging
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
mongodb-staging:
image: ghcr.io/bluewave-labs/checkmate:mongo-staging
container_name: mongodb-staging
restart: always
command: [ "mongod", "--quiet", "--bind_ip_all" ]
volumes:
- ./mongo/data-staging:/data/db
env_file:
- mongo-staging.env
healthcheck:
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ]
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
# Shared Utils
capture:
image: ghcr.io/bluewave-labs/capture:latest
container_name: capture
restart: unless-stopped
ports:
- "59232:59232"
environment:
- API_SECRET=my_secret
- GIN_MODE=release
volumes:
- /etc/os-release:/etc/os-release:ro
+55
View File
@@ -0,0 +1,55 @@
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
server {
listen 80;
server_name demo.checkmate.so staging.checkmate.so;
location /.well-known/acme-challenge/ {
# Forward the challenge to Certbot listening on 8080
proxy_pass http://172.17.0.1:8080;
}
location / {
return 301 https://$host$request_uri;
}
}
# --- PRODUCTION: demo.checkmate.so ---
server {
listen 443 ssl;
server_name demo.checkmate.so;
ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# Passes traffic to the prod client on port 80
proxy_pass http://client-prod:80;
}
}
# --- STAGING: staging.checkmate.so ---
server {
listen 443 ssl;
server_name staging.checkmate.so;
ssl_certificate /etc/nginx/ssl/live/staging.checkmate.so/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/staging.checkmate.so/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# Passes traffic to the staging client on port 80
proxy_pass http://client-staging:80;
}
}
+3
View File
@@ -0,0 +1,3 @@
FROM mongo
EXPOSE 27017
CMD ["mongod"]
+31
View File
@@ -0,0 +1,31 @@
server {
listen 80;
listen [::]:80;
server_name _;
server_tokens off;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server-prod:52345/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server-prod:52345/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+31
View File
@@ -0,0 +1,31 @@
server {
listen 80;
listen [::]:80;
server_name _;
server_tokens off;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server-staging:52345/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server-staging:52345/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+22
View File
@@ -0,0 +1,22 @@
FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_OPTIONS="--max-old-space-size=2048"
WORKDIR /app
COPY ./server/package*.json ./
RUN npm install
COPY ./server ./
RUN npm run build
EXPOSE 52345
CMD ["node", "dist/index.js"]
+31
View File
@@ -0,0 +1,31 @@
# Contributing Translations
Checkmate supports multiple languages. This guide explains how to contribute translations.
## How to Add a New Language
1. Create a new JSON file in `client/src/locales/` named with the language code (e.g., `it.json` for Italian)
2. Copy the structure from `en.json` and translate all values
3. Add the language to the language selector in the UI
## Translation Guidelines
- en.json is the reference localization file, all keys should be declared here and mirrored in other localization files
- Translate only the values
- Use formal/informal tone consistently
- Test your translations in the UI
## File Structure
client/src/locales/
├── en.json # Source language
├── es.json # Spanish
├── de.json # German
└── [lang].json # Your language
## Testing Translations
1. Start the development server: `npm run dev`
2. Go to Settings → Language
3. Select your language
4. Verify all strings display correctly
+1 -1
View File
@@ -8,7 +8,7 @@ Checkmate's backend is designed to be lightweight and scalable, ensuring reliabl
## Installation & documentation
For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://docs.checkmate.so).
For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://checkmate.so/docs).
## Related repositories
+4 -1
View File
@@ -32,11 +32,14 @@ export default [
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
projectService: {
allowDefaultProject: ["jest.config.ts"],
},
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
},
+1 -2
View File
@@ -9,8 +9,7 @@ const config: Config = {
},
moduleNameMapper: {
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
"^@/utils/(AppError)\\.js$": "<rootDir>/src/utils/$1.ts",
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.js",
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.ts",
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
"^@/(.*)$": "<rootDir>/src/$1",
+174 -137
View File
@@ -510,42 +510,24 @@
"summary": "Get monitors by team",
"description": "Get monitors filtered by team with optional parameters",
"parameters": [
{
"name": "status",
"in": "query",
"description": "Filter by monitor status (up/down)",
"schema": {
"type": "boolean"
}
},
{
"name": "type",
"in": "query",
"description": "Filter by monitor type",
"schema": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"]
}
},
{
"name": "page",
"in": "query",
"description": "Page number for pagination",
"schema": {
"type": "integer",
"minimum": 1,
"default": 1
}
},
{
"name": "rowsPerPage",
"in": "query",
"description": "Number of monitors per page",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
"oneOf": [
{
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
},
{
"type": "array",
"items": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
}
}
]
}
},
{
@@ -555,15 +537,6 @@
"schema": {
"type": "string"
}
},
{
"name": "field",
"in": "query",
"description": "Field to filter on",
"schema": {
"type": "string",
"enum": ["name", "url", "description"]
}
}
],
"responses": {
@@ -756,7 +729,7 @@
}
]
},
"put": {
"patch": {
"tags": ["monitors"],
"summary": "Update monitor",
"description": "Update an existing monitor",
@@ -3568,22 +3541,19 @@
},
"CreateMonitorRequest": {
"type": "object",
"required": ["name", "description", "type", "url", "interval"],
"required": ["name", "type", "url"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"example": "My Website Monitor"
},
"description": {
"type": "string",
"maxLength": 500,
"example": "Monitors the main website homepage"
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"],
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"],
"example": "http"
},
"url": {
@@ -3593,14 +3563,34 @@
},
"interval": {
"type": "integer",
"minimum": 30,
"maximum": 3600,
"example": 300,
"description": "Check interval in seconds"
},
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
},
"isActive": {
"type": "boolean"
},
"ignoreTlsErrors": {
"type": "boolean",
"default": true
"default": false
},
"useAdvancedMatching": {
"type": "boolean",
"default": false
},
"port": {
"type": "integer"
},
"notifications": {
"type": "array",
@@ -3609,51 +3599,62 @@
},
"description": "Array of notification IDs to associate with this monitor"
},
"httpOptions": {
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"],
"default": "GET"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"body": {
"type": "string",
"description": "Request body for POST/PUT requests"
},
"timeout": {
"type": "integer",
"minimum": 1000,
"maximum": 30000,
"default": 5000,
"description": "Request timeout in milliseconds"
}
}
"secret": {
"type": "string"
},
"assertions": {
"jsonPath": {
"type": "string"
},
"expectedValue": {
"type": "string"
},
"matchMethod": {
"type": "string",
"enum": ["equal", "include", "regex", ""]
},
"cpuAlertThreshold": {
"type": "number"
},
"memoryAlertThreshold": {
"type": "number"
},
"diskAlertThreshold": {
"type": "number"
},
"tempAlertThreshold": {
"type": "number"
},
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string",
"default": ""
},
"selectedDisks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
"type": "string"
}
},
"group": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array",
"items": {
"type": "string",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
}
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
}
}
},
@@ -3661,21 +3662,32 @@
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string",
"minLength": 1,
"maxLength": 100
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
},
"url": {
"type": "string"
},
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
},
"description": {
"type": "string",
"maxLength": 500
"type": "string"
},
"interval": {
"type": "integer",
"minimum": 30,
"maximum": 3600
},
"isActive": {
"type": "boolean"
"type": "integer"
},
"notifications": {
"type": "array",
@@ -3683,47 +3695,72 @@
"type": "string"
}
},
"httpOptions": {
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"]
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"body": {
"type": "string"
},
"timeout": {
"type": "integer",
"minimum": 1000,
"maximum": 30000
}
}
"secret": {
"type": "string"
},
"assertions": {
"ignoreTlsErrors": {
"type": "boolean"
},
"useAdvancedMatching": {
"type": "boolean"
},
"jsonPath": {
"type": "string"
},
"expectedValue": {
"type": "string"
},
"matchMethod": {
"type": "string",
"enum": ["equal", "include", "regex", ""]
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"cpuAlertThreshold": {
"type": "number"
},
"memoryAlertThreshold": {
"type": "number"
},
"diskAlertThreshold": {
"type": "number"
},
"tempAlertThreshold": {
"type": "number"
},
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string"
},
"selectedDisks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
"type": "string"
}
},
"group": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array",
"items": {
"type": "string",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
}
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
}
}
},
+726 -1004
View File
File diff suppressed because it is too large. Load diff
+12 -9
View File
@@ -5,17 +5,20 @@
"main": "index.js",
"type": "module",
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/**/*.ts' test/unit",
"test:unit:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services test/unit/providers/network test/unit/providers/notifications",
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --no-coverage test/integration",
"dev": "nodemon --exec tsx src/index.js",
"start": "node --watch ./dist/index.js",
"build": "tsc && tsc-alias",
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"format": "prettier --write .",
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "npm run format"
"**/*": "prettier --write"
},
"keywords": [],
"author": "",
@@ -29,7 +32,6 @@
"@pulsecron/pulse": "1.6.8",
"axios": "^1.7.2",
"bcryptjs": "3.0.2",
"bullmq": "5.41.2",
"cacheable-lookup": "7.0.0",
"compression": "1.8.1",
"cookie-parser": "^1.4.7",
@@ -37,8 +39,8 @@
"dockerode": "4.0.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "8.0.1",
"gamedig": "^5.3.1",
"express-rate-limit": "8.3.2",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
"helmet": "^8.0.0",
@@ -51,13 +53,14 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"nodemailer": "8.0.5",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.4.5",
"super-simple-scheduler": "1.9.1",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0",
"ws": "^8.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -78,9 +81,9 @@
"@types/papaparse": "^5.5.2",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"c8": "10.1.3",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
Loaded 100 of 290 files, more files were not shown because too many files have changed in this diff. Show more