Compare commits

..
Author SHA1 Message Date
Alex Holliday a236bec6c1 notification channel fix 2026-01-08 18:55:17 +00:00
Alex Holliday b4d127c6ce role fix 2026-01-08 18:53:11 +00:00
Alex Holliday 0dd977eeef refactore role service 2026-01-08 18:51:56 +00:00
Alex Holliday 58a77d6aa2 refactor to use repository 2026-01-08 17:47:26 +00:00
Alex Holliday c42932a7b3 refactor 2026-01-08 17:27:02 +00:00
Alex Holliday c1bb1fd29d refactor me service 2026-01-07 22:18:48 +00:00
Alex Holliday 03ce8f84ea fe 2026-01-07 21:39:32 +00:00
Alex Holliday 77f943a7c7 be 2026-01-07 21:11:44 +00:00
Alex Holliday 9248870716 auth service 2026-01-07 19:50:06 +00:00
Alex Holliday a3e24b5420 refactor auth service 2026-01-07 00:00:50 +00:00
Alex Holliday 3611aee468 ts fixes 2026-01-06 23:04:30 +00:00
Alex Holliday 61bf2a0dfb use repos 2026-01-06 22:53:54 +00:00
Alex Holliday 052b9377bc delete many 2026-01-06 21:08:41 +00:00
Alex Holliday ba7cc3bb0c ts errors 2026-01-06 20:55:27 +00:00
Alex Holliday 51081a6a0e incidents repo and data reversal 2026-01-06 20:49:06 +00:00
Alex Holliday 605b4713af ts errors 2026-01-05 23:50:34 +00:00
Alex Holliday 81d6c4651f CheckService 2026-01-05 23:14:19 +00:00
Alex Holliday 74f22a3e70 fix date tick 2026-01-05 21:23:15 +00:00
Alex Holliday ce568424a3 fix remaining type issues 2026-01-05 21:12:28 +00:00
Alex Holliday 9583332852 data utils fix 2026-01-05 20:23:20 +00:00
Alex Holliday afb30dceff up/down check fix 2026-01-05 19:57:56 +00:00
Alex Holliday 7aae57e9ab refactor monitor stats 2026-01-05 19:45:30 +00:00
Alex Holliday 36cdb20dfc types 2026-01-05 17:58:47 +00:00
Alex Holliday e62e4898b9 notificaitons 2026-01-05 04:35:01 +00:00
Alex Holliday 3182141d0a GroupedChecks 2026-01-05 04:17:11 +00:00
Alex Holliday cdc58e5905 import 2026-01-04 22:44:39 +00:00
Alex Holliday 7ef1803cf0 getallembedchecks 2026-01-04 22:24:55 +00:00
Alex Holliday 6e341108bb inject check repo 2026-01-03 21:16:46 +00:00
Alex Holliday af6c242a18 Create repo and domain model 2026-01-03 20:58:37 +00:00
Alex Holliday e2b012fdde auth service 2026-01-03 20:47:02 +00:00
Alex Holliday e5ef14215f status pages 2026-01-03 20:32:16 +00:00
Alex Holliday 602597abe2 jobqueue and stauts service 2026-01-02 23:28:36 +00:00
Alex Holliday 3bdfe5567d Rename methods 2026-01-02 22:20:43 +00:00
Alex Holliday 56aa7b3e1a refactor service 2026-01-02 22:16:00 +00:00
Alex Holliday 82cb59ae9e docker, infra, and pagespeed 2026-01-02 20:53:50 +00:00
Alex Holliday 8e6a8e5a06 uptime 2026-01-02 20:33:34 +00:00
Alex Holliday 7f15811fdf defensive checks 2025-12-31 20:51:30 +00:00
Alex Holliday 86cfc89545 docker details tweaks 2025-12-30 22:21:51 +00:00
Alex Holliday 5c9d6b5523 server 2025-12-30 21:50:38 +00:00
Alex Holliday 8b079905a3 client 2025-12-30 21:47:03 +00:00
Alex Holliday 7a01a8a8a3 stauts 2025-12-30 20:30:20 +00:00
Alex Holliday f56f91c31d rollup 2025-12-30 19:49:13 +00:00
Alex Holliday dea0ff106f health and status 2025-12-30 19:27:26 +00:00
Alex Holliday 445b594cab client 2025-12-30 19:23:48 +00:00
Alex Holliday 4f4863db75 details barebones 2025-12-29 23:00:39 +00:00
Alex Holliday a4d818e0c5 docker monitors skeleotn 2025-12-29 20:22:28 +00:00
Alex Holliday 378a40ce84 backend implementation 2025-12-29 19:52:22 +00:00
Alex Holliday a250bf38aa Merge branch 'fix/form-cleanup' into feat/saas 2025-12-29 18:32:18 +00:00
Alex Holliday 18408a6bf6 finish form extraction 2025-12-29 18:28:30 +00:00
Alex Holliday 6dcfb67246 status page 2025-12-28 19:49:36 +00:00
Alex Holliday 8fb13273bf settings 2025-12-28 19:36:14 +00:00
Alex Holliday a2454cbab6 teams 2025-12-28 19:25:51 +00:00
Alex Holliday 15ceb4af46 teams 2025-12-28 19:20:06 +00:00
Alex Holliday 24cbcec2c9 maintenance 2025-12-28 19:12:35 +00:00
Alex Holliday e55ae64222 infra and notification 2025-12-28 18:59:05 +00:00
Alex Holliday 4dfeb71a9d infra refactor 2025-12-28 18:35:14 +00:00
Alex Holliday df707ab194 pagespeed cleanup 2025-12-28 07:05:09 +00:00
Alex Holliday 16aec18c28 refactor uptime forms' 2025-12-28 01:48:08 +00:00
Alex Holliday de39688663 Merge branch 'feat/saas' of github.com:bluewave-labs/Checkmate into feat/saas 2025-12-28 00:10:33 +00:00
Alex Holliday 4ed89ace23 restore loading state 2025-12-27 16:09:02 +00:00
Alex Holliday 7961a9a88e cleanup register and login 2025-12-27 16:07:57 +00:00
Alex Holliday 805ee93914 translations 2025-12-23 13:50:24 -08:00
Alex Holliday a967c12ba5 Merge branch 'feat/saas' of github.com:bluewave-labs/Checkmate into feat/saas 2025-12-23 20:10:43 +00:00
Alex Holliday 3f8198f3df remove unused indexes 2025-12-23 20:10:18 +00:00
Alex Holliday 658c7c090f error fixes 2025-12-23 19:54:00 +00:00
Alex Holliday 4667c59573 improve checks fetch and paginaiton 2025-12-23 19:26:07 +00:00
Alex Holliday 6978a7e2bb check indexes 2025-12-23 18:15:59 +00:00
Alex Holliday 7f33251961 checks table message 2025-12-22 16:53:05 -08:00
Alex Holliday 46740aa4b9 ts 2025-12-23 00:36:03 +00:00
Alex Holliday 92fd405395 check card details 2025-12-23 00:34:40 +00:00
Alex Holliday 9f32eb6362 legend 2025-12-22 17:59:11 +00:00
Alex Holliday b200157f28 chart 2025-12-22 16:28:14 +00:00
Alex Holliday 6323f70bd2 check details 2025-12-22 06:39:00 +00:00
Alex Holliday 7681edbf49 navigate on click 2025-12-21 19:37:43 +00:00
Alex Holliday 66c406151a check details base 2025-12-19 15:12:30 -08:00
Alex Holliday d139e0b643 cleanup 2025-12-19 22:28:08 +00:00
Alex Holliday 1a8a18248b cleanup 2025-12-19 20:27:24 +00:00
Alex Holliday 26ed713923 extract getStartState 2025-12-19 19:10:06 +00:00
Alex Holliday 9fa2980204 Remove deprecated dateFormat 2025-12-19 18:54:53 +00:00
Alex Holliday 0535ddbc38 fix 1h range 2025-12-19 18:52:07 +00:00
Alex Holliday caca71dce5 2h -> 1h 2025-12-19 18:41:01 +00:00
Alex Holliday 08d53b80b7 extract methods 2025-12-19 18:26:24 +00:00
Alex Holliday d8007a9a5f fix peer cert 2025-12-19 18:18:42 +00:00
Alex Holliday df95ee7687 temporarily comment out cert 2025-12-19 09:26:02 -08:00
Alex Holliday e07c82611c disable cert expiry 2025-12-18 14:46:56 -08:00
Alex Holliday e76fc05f8d weakset for sockets 2025-12-18 21:37:09 +00:00
Alex Holliday 4d7c5ca885 Merge branch 'feat/saas' of github.com:bluewave-labs/Checkmate into feat/saas 2025-12-18 21:21:25 +00:00
Alex Holliday 61d40c7cd8 reuse socket listener 2025-12-18 21:21:18 +00:00
Alex Holliday 88dc2f4096 format x-axis 2025-12-18 12:57:05 -08:00
Alex Holliday 6f41167456 missing x axis 2025-12-18 12:53:16 -08:00
Alex Holliday 40977775f0 fix conditional rendering 2025-12-18 12:45:34 -08:00
Alex Holliday 93dd49c875 fix breadcrumb 2025-12-18 12:41:43 -08:00
Alex Holliday a2db7b46d6 date trunc 2025-12-18 19:20:06 +00:00
Alex Holliday 58f5896d15 conditional sort, bounded end date 2025-12-18 19:17:32 +00:00
Alex Holliday aa346abf5b indexes 2025-12-18 19:04:44 +00:00
Alex Holliday 1ee8a873f6 Merge branch 'feat/saas' of github.com:bluewave-labs/Checkmate into feat/saas 2025-12-18 18:50:47 +00:00
Alex Holliday fdd935c676 check idx 2025-12-18 18:50:20 +00:00
Alex Holliday d12a6cce8a cleanup StatsAggregation 2025-12-17 16:04:18 -08:00
Alex Holliday 35a6d5711d remove logs 2025-12-17 15:13:08 -08:00
Alex Holliday 8c9a4bd6ec logs 2025-12-17 23:08:19 +00:00
Alex Holliday 3f33b302f9 midbucket date 2025-12-17 22:44:39 +00:00
Alex Holliday 96567a9818 tests 2025-12-17 22:26:03 +00:00
Alex Holliday 5489a107f9 diagnostics locale 2025-12-17 14:18:06 -08:00
Alex Holliday 9f695ecdcc ts error 2025-12-17 21:13:34 +00:00
Alex Holliday c8dc5b93cb aggregates 2025-12-17 21:11:51 +00:00
Alex Holliday 95e415ed13 comments 2025-12-17 19:27:20 +00:00
Alex Holliday 59e85f7613 hourly stats and daily rollup 2025-12-17 19:20:03 +00:00
Alex Holliday e1db8ea513 debian 2025-12-16 23:51:34 +00:00
Alex Holliday f9f94cc33c qemu 2025-12-16 23:46:00 +00:00
Alex Holliday a17309e17c job name 2025-12-16 23:37:25 +00:00
Alex Holliday 57d714ac59 SSH 2025-12-16 23:36:38 +00:00
Alex Holliday 60e50cb5db action 2025-12-16 23:19:04 +00:00
Alex Holliday aa282439fc Fix autocomlete alignment 2025-12-16 21:49:03 +00:00
Alex Holliday f92d08810a stripe service test 2025-12-16 21:20:19 +00:00
Alex Holliday 9222d0b62d setting sservices tests 2025-12-16 21:17:29 +00:00
Alex Holliday 4b1e8fb002 Remove unused service registry 2025-12-16 21:12:58 +00:00
Alex Holliday 837c4598ff loading state 2025-12-16 20:37:25 +00:00
Alex Holliday 0b626f991f move ApiResponse 2025-12-16 19:09:08 +00:00
Alex Holliday 03b6d5976c remove me from auth controller 2025-12-16 18:40:13 +00:00
Alex Holliday 968eb6a2a8 skeleton card colors 2025-12-16 18:26:20 +00:00
Alex Holliday 465e673f4b empty view updated 2025-12-16 18:15:47 +00:00
Alex Holliday 422d13ef25 udpate /me 2025-12-15 23:36:50 +00:00
Alex Holliday 666d522ee9 fc -> plain 2025-12-15 22:19:00 +00:00
Alex Holliday 660346d2bc close sidebar on click 2025-12-15 20:42:33 +00:00
Alex Holliday 07b08d0037 paginaiton fix 2025-12-15 20:31:37 +00:00
Alex Holliday 71575a9d86 useTeamIdAsKey 2025-12-15 19:48:38 +00:00
Alex Holliday e4183baf75 reload on team change 2025-12-15 19:39:23 +00:00
Alex Holliday 4e3630465c infra error handling 2025-12-15 19:24:38 +00:00
Alex Holliday 2e523cc431 render strings 2025-12-15 19:13:20 +00:00
Alex Holliday 1a2d11e6de refactor /auth/me into /me 2025-12-14 17:16:21 +00:00
Alex Holliday 4b92b2a8df entitlements factory 2025-12-14 16:41:38 +00:00
Alex Holliday fc2c3aa6a6 teams form 2025-12-14 00:35:39 +00:00
Alex Holliday 2eb6850cb9 diagnostics access 2025-12-14 00:24:53 +00:00
Alex Holliday 6f99249d62 client 2025-12-13 23:12:02 +00:00
Alex Holliday 9aa87f57ae Fe 2025-12-13 22:04:32 +00:00
Alex Holliday 16c7a007d5 env 2025-12-13 16:58:30 +00:00
Alex Holliday 92f7a929fa billing 2025-12-12 14:56:54 -08:00
Alex Holliday 5e9c85f372 billing 2025-12-12 14:54:20 -08:00
Alex Holliday 901b5a43c8 auth 2025-12-12 12:17:18 -08:00
Alex Holliday c9dea5c0ae strings 2025-12-12 10:54:26 -08:00
Alex Holliday 22f7d002fd notifications 2025-12-11 15:20:21 -08:00
Alex Holliday 63dd455e4a infra locale 2025-12-11 13:30:43 -08:00
Alex Holliday 64d1ba08f9 locale 2025-12-11 12:50:30 -08:00
Alex Holliday b94e6052c3 strings 2025-12-11 10:55:17 -08:00
Alex Holliday d6cddbd812 locale reset 2025-12-11 18:48:28 +00:00
Alex Holliday fe03451393 status page pagination 2025-12-11 18:41:38 +00:00
Alex Holliday b6a3d1e34d refactor entitlements 2025-12-11 18:07:22 +00:00
Alex Holliday 9bfe146e2b index 2025-12-10 19:58:50 +00:00
Alex Holliday c7945aa4a9 dockerfile 2025-12-10 19:26:24 +00:00
Alex Holliday 0d41e7229b env 2025-12-10 19:14:07 +00:00
Alex Holliday 8d3d764a0d config 2025-12-10 19:01:59 +00:00
Alex Holliday 35679fb433 gitignore 2025-12-10 18:23:07 +00:00
Alex Holliday 4eb2f0bee3 assets 2025-12-10 18:22:52 +00:00
Alex Holliday 2cb803948c dockerfile 2025-12-10 17:47:02 +00:00
Alex Holliday 85589d6d3a dockerfile 2025-12-10 17:43:35 +00:00
Alex Holliday 93317cbba7 ts 2025-12-09 22:11:19 +00:00
Alex Holliday 8ae7638432 logo adjustment, remove console.log 2025-12-09 22:10:07 +00:00
Alex Holliday 4f01ed7019 config trim 2025-12-09 21:27:23 +00:00
Alex Holliday b4e4ec1110 ts 2025-12-09 21:15:27 +00:00
Alex Holliday bda3a7e91c loading indicators 2025-12-09 21:01:47 +00:00
Alex Holliday 03d681f212 log 2025-12-09 19:24:48 +00:00
Alex Holliday 69ea8eaf6b table 2025-12-09 19:01:56 +00:00
Alex Holliday bd14ea3461 fix skeleton 2025-12-09 18:49:57 +00:00
Alex Holliday 3616b69925 pagespeed 2025-12-08 23:55:27 +00:00
Alex Holliday e12e198aed export team incidents 2025-12-08 23:18:46 +00:00
Alex Holliday 36208aacea pagespeed fixes 2025-12-08 22:19:07 +00:00
Alex Holliday f78ea7ab9c remove ripples 2025-12-08 22:12:07 +00:00
Alex Holliday a8e55db134 fix buttons 2025-12-08 22:08:22 +00:00
Alex Holliday 50b734cf24 sidebar 2025-12-08 21:58:35 +00:00
Alex Holliday a3f7ac8ae3 dialogs 2025-12-08 21:40:04 +00:00
Alex Holliday 4525f8035b status box update and res card update 2025-12-08 21:25:33 +00:00
Alex Holliday 8274eb2ec9 fix logo 2025-12-08 21:13:37 +00:00
Alex Holliday b6c699c141 finish nuking theme 2025-12-08 21:10:29 +00:00
Alex Holliday dd463bb60b nuke theme 2025-12-08 19:55:29 +00:00
Alex Holliday 32fbb8f434 new palette 2025-12-08 18:43:22 +00:00
Alex Holliday 28ebdcb216 update incident table 2025-12-08 18:19:07 +00:00
Alex Holliday c762f9425a cleanup 2025-12-08 18:13:49 +00:00
Alex Holliday b3206f4011 resolutionc ard width 2025-12-08 17:43:02 +00:00
Alex Holliday c0a8398baa incident links 2025-12-08 17:22:17 +00:00
Alex Holliday 2b3c601a05 resolution card 2025-12-08 02:29:52 +00:00
Alex Holliday b4135e26ce details 2025-12-07 04:38:37 +00:00
Alex Holliday 0453b790af infra thresholds FE 2025-12-07 03:34:33 +00:00
Alex Holliday 7631e65cd2 thresholds be 2025-12-05 22:46:46 +00:00
Alex Holliday 2ab983eff5 evaluate threshold 2025-12-05 01:13:23 +00:00
Alex Holliday 75ef6c934d add monitor thresholds 2025-12-04 21:52:50 +00:00
Alex Holliday ba6c9a481d most popular chip 2025-12-04 20:26:21 +00:00
Alex Holliday 5c889f3a63 plan card tweaks 2025-12-03 21:21:00 +00:00
Alex Holliday c088956d6f numeric values 2025-12-03 20:49:00 +00:00
Alex Holliday 30d7b3d258 fix entitlements on login 2025-12-02 21:46:01 +00:00
Alex Holliday 53355cad04 price cards 2025-12-02 20:52:27 +00:00
Alex Holliday 07efd08837 billing tweaks 2025-12-02 19:12:19 +00:00
Alex Holliday a9a598941f fix 2025-12-02 17:44:44 +00:00
Alex Holliday ad2954e326 fix 2025-12-02 17:33:16 +00:00
Alex Holliday d77de3b6bc logging 2025-12-02 17:24:42 +00:00
Alex Holliday e0e260be8f success page 2025-12-02 17:00:37 +00:00
Alex Holliday 634885c362 update socket 2025-12-02 00:31:21 +00:00
Alex Holliday 1b162578c2 stripe 2025-12-02 00:20:09 +00:00
Alex Holliday 01c0e9ef4d create customer 2025-12-01 03:45:57 +00:00
Alex Holliday 7b207f2ccb fe 2025-12-01 00:35:53 +00:00
Alex Holliday 6bca0ed00b billing skeleton 2025-11-30 17:16:30 +00:00
Alex Holliday d76da4b00c relocate permissions 2025-11-30 17:08:31 +00:00
Alex Holliday 6eabaebd86 stripe controller 2025-11-30 16:52:48 +00:00
Alex Holliday f476a57748 protect routes and import function 2025-11-29 19:24:08 +00:00
Alex Holliday e600f293b0 entitlements FE 2025-11-28 22:48:32 +00:00
Alex Holliday c43241eb52 entitlementsBE 2025-11-28 21:31:44 +00:00
Alex Holliday 86ce30b5f4 admin settings 2025-11-28 19:48:57 +00:00
Alex Holliday 08887bbc58 checks retention routes 2025-11-27 23:37:29 +00:00
Alex Holliday ce97e201c9 correct check ttl 2025-11-27 23:05:58 +00:00
Alex Holliday 229027830d udpate tests 2025-11-27 21:23:09 +00:00
Alex Holliday 5876b97ede reject unauthorized 2025-11-27 20:41:49 +00:00
Alex Holliday 686b96d4b3 ssl 2025-11-27 19:39:26 +00:00
Alex Holliday ee04ac2b98 delete all 2025-11-26 23:57:40 +00:00
Alex Holliday 6c04f46e8a fix deps 2025-11-26 23:17:41 +00:00
Alex Holliday 8774042725 search 2025-11-26 22:22:15 +00:00
Alex Holliday c50b0cbfd6 ts 2025-11-26 13:51:44 -08:00
Alex Holliday 5780c0b969 button groups 2025-11-26 21:37:12 +00:00
Alex Holliday 36d13d556e test notifications 2025-11-26 20:40:21 +00:00
Alex Holliday cb5d8c3468 parallelization 2025-11-26 19:56:47 +00:00
Alex Holliday 4e8281aefb parallelize notifications 2025-11-26 19:26:16 +00:00
Alex Holliday 92750ce8f9 native props 2025-11-26 18:57:51 +00:00
Alex Holliday 5f43c23b26 fixes for allback 2025-11-26 18:56:48 +00:00
Alex Holliday 746ed4a3c1 fix fallback 2025-11-26 18:46:58 +00:00
Alex Holliday 816a1607a1 middleware 2025-11-26 17:52:39 +00:00
Alex Holliday 3bffe61b42 border 2025-11-25 23:58:30 +00:00
Alex Holliday 33aab936fe radio 2025-11-25 23:37:56 +00:00
Alex Holliday b5bdd73a90 settings 2025-11-25 23:07:16 +00:00
Alex Holliday 4a79013ef2 gap 2025-11-25 20:32:58 +00:00
Alex Holliday a8d83b33b4 add heatmap chart 2025-11-25 19:56:26 +00:00
Alex Holliday c2a1d6d97e use heathmap 2025-11-25 19:03:48 +00:00
Alex Holliday 68ab185f84 404 2025-11-24 22:55:37 +00:00
Alex Holliday 82dbac9ac9 error pages 2025-11-24 22:50:33 +00:00
Alex Holliday 1e26fb1e3b improve test coverage 2025-11-24 22:49:55 +00:00
Alex Holliday d5f1032bdf Add message to check table 2025-11-24 20:04:07 +00:00
Alex Holliday db64ad1de4 add additional tests 2025-11-24 20:03:06 +00:00
Alex Holliday ef4ca52285 Fix tests 2025-11-24 19:19:27 +00:00
Alex Holliday e0d4f0d0f6 Add init box 2025-11-24 19:13:38 +00:00
Alex Holliday f7490ffe64 consistent spacing for autocomplete 2025-11-24 19:03:06 +00:00
Alex Holliday 9be77a547d checkboxes 2025-11-24 18:54:52 +00:00
Alex Holliday e4d6e0930f fix single item display 2025-11-24 18:42:30 +00:00
Alex Holliday 2319b67d05 add placeholder 2025-11-24 18:07:47 +00:00
Alex Holliday 94ecaddae2 ts 2025-11-21 15:48:06 -08:00
Alex Holliday e032e4b9b6 ts 2025-11-21 15:46:07 -08:00
Alex Holliday b88c46a6a1 fix invite 2025-11-21 23:44:20 +00:00
Alex Holliday 3c71583324 permissions on FE for email settings 2025-11-21 14:26:41 -08:00
Alex Holliday 710d506de2 fix middleware 2025-11-21 13:59:29 -08:00
Alex Holliday c92c7fb9c1 ts errors 2025-11-21 21:06:16 +00:00
Alex Holliday 3a1fb61847 docker 2025-11-21 21:00:36 +00:00
Alex Holliday 9bce91c20d incident cleanup 2025-11-21 20:18:34 +00:00
Alex Holliday 1683c2980e round port resonse time 2025-11-21 20:06:11 +00:00
Alex Holliday 5d825537ae implement port monitoring 2025-11-21 20:03:03 +00:00
Alex Holliday caa863afe8 add loading state to dialog 2025-11-20 23:34:05 +00:00
Alex Holliday 6881be7555 monitor import/export 2025-11-20 23:27:30 +00:00
Alex Holliday af6fede1b3 email config 2025-11-20 21:17:52 +00:00
Alex Holliday 60153a88e2 email env priority 2025-11-20 19:27:22 +00:00
Alex Holliday 3faff5dc54 email settings 2025-11-19 23:55:29 +00:00
Alex Holliday 819c0ca05a default sort 2025-11-19 21:01:45 +00:00
Alex Holliday 459d78cbcb sorting 2025-11-19 20:53:25 +00:00
Alex Holliday eea6b12be6 isActive -> stauts === paused 2025-11-19 19:06:17 +00:00
Alex Holliday 196ebc30dc isActive -> stauts === paused 2025-11-19 19:06:04 +00:00
Alex Holliday f40667615e system settings 2025-11-19 00:00:08 +00:00
Alex Holliday 986e8b926a featur eflags 2025-11-18 23:11:25 +00:00
Alex Holliday bab99059c5 simplify 2025-11-18 21:49:07 +00:00
Alex Holliday adbb4e7817 implement monitor filter 2025-11-18 21:43:35 +00:00
Alex Holliday a40277be4a udpate server 2025-11-14 15:56:52 -08:00
Alex Holliday dd302c08b3 fixes 2025-11-14 15:35:37 -08:00
Alex Holliday 55784c93de cleanup 2025-11-14 15:30:02 -08:00
Alex Holliday 3b738100ec remove files 2025-11-14 15:22:58 -08:00
Alex Holliday c7046be642 Merge branch 'feat/saas' of github.com:bluewave-labs/Checkmate into feat/saas 2025-11-14 23:19:39 +00:00
Alex Holliday 841a9832ec fix build 2025-11-14 23:19:11 +00:00
Alex Holliday 6379a66a8d dependencies 2025-11-14 14:55:23 -08:00
Alexander Holliday f8cea8b075 Merge pull request #3065 from bluewave-labs/feat/tests
Feat/tests
2025-11-14 14:49:54 -08:00
Alex Holliday 29b68b8c87 test case 2025-11-14 22:48:48 +00:00
Alex Holliday 30ce3f6bf4 requestStatus 2025-11-14 22:15:14 +00:00
Alex Holliday 313121d9a4 requestPing 2025-11-14 22:10:20 +00:00
Alex Holliday 04c9b28216 requestInfra 2025-11-14 21:54:44 +00:00
Alex Holliday c9d33c4292 add tests 2025-11-14 19:10:34 +00:00
Alex Holliday b4da045770 add test env 2025-11-14 17:17:41 +00:00
Alex Holliday 75acf7b38f initial commit 2025-11-13 13:47:26 -08:00
1251 changed files with 63196 additions and 160649 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
**/node_modules
**/dist
**/.env
**/*.env
.git
docker/dev/mongo/data
-2
View File
@@ -1,2 +0,0 @@
* text=auto
* text eol=lf
+1 -1
View File
@@ -1 +1 @@
* @ajhollid @mohicody @Br0wnHammer @Owaiseimdad @karenvicent @shanikauwu1
+6 -7
View File
@@ -1,28 +1,27 @@
**(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-reviewing and testing of my code.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-review 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.
-103
View File
@@ -1,103 +0,0 @@
name: Build image
on:
push:
branches: ["develop", "master"]
tags: ["v*"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# checkmate-backend-mono is a transitional alias so existing deployments
# keep receiving updates; drop it after one release cycle.
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/checkmate
ghcr.io/${{ github.repository_owner }}/checkmate-backend-mono
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# The hosted staging/prod instances share one host and one compose file; each
# deploy pulls and recreates only its own service.
deploy-staging:
needs: build-and-push
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy staging.checkmate.so
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docs/hosting/checkmate-so
docker compose pull checkmate-staging
docker compose up -d checkmate-staging
docker system prune -af
deploy-prod:
needs: build-and-push
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Deploy demo.checkmate.so
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docs/hosting/checkmate-so
docker compose pull checkmate-demo
docker compose up -d checkmate-demo
docker system prune -af
-22
View File
@@ -1,22 +0,0 @@
name: Lockfile Check (Client & Server)
on:
pull_request:
workflow_dispatch:
jobs:
check-lockfiles:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Validate with the same node image the production build uses, so a lock
# that passes here cannot fail the image build's npm ci.
- name: Resolve node image from Dockerfile
id: node-image
run: echo "image=$(grep -m1 -o 'node:[^ ]*' docker/Dockerfile)" >> "$GITHUB_OUTPUT"
- name: Check server lockfile
run: docker run --rm -v "$PWD/server":/w -w /w ${{ steps.node-image.outputs.image }} npm ci --dry-run
- name: Check client lockfile
run: docker run --rm -v "$PWD/client":/w -w /w ${{ steps.node-image.outputs.image }} npm ci --dry-run
-23
View File
@@ -1,23 +0,0 @@
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
@@ -0,0 +1,75 @@
name: Build images (feat/saas)
on:
push:
branches: ["feat/saas"]
workflow_dispatch:
env:
PLATFORMS: linux/amd64,linux/arm64
TAG: latest
IMAGE_CLIENT: ghcr.io/ajhollid/checkmate-saas-client
IMAGE_SERVER: ghcr.io/ajhollid/checkmate-saas-server
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU (for multi-arch emulation)
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx (multiarch)
uses: docker/setup-buildx-action@v3
with:
install: true
driver-opts: |
network=host
buildkitd-flags: --debug
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_AJHOLLID_TOKEN }}
- name: Build and push Client (multi-arch)
uses: docker/build-push-action@v5
with:
context: client
file: client/Dockerfile
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ env.IMAGE_CLIENT }}:${{ env.TAG }}
- name: Build and push Server (multi-arch)
uses: docker/build-push-action@v5
with:
context: server
file: server/Dockerfile
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ env.IMAGE_SERVER }}:${{ env.TAG }}
deploy-to-ajhollid:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.AJHOLLID_HOST }}
username: ${{ secrets.AJHOLLID_USER }}
key: ${{ secrets.AJHOLLID_SSH_PRIVATE_KEY }}
script: |
cd checkmate-saas
docker compose down
docker compose pull
docker compose up -d
docker system prune -af
@@ -0,0 +1,147 @@
name: Deploy images on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }}
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }}
+142
View File
@@ -0,0 +1,142 @@
name: Deploy images
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:latest \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:latest
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:latest \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:latest
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:latest \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:latest
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ env.VERSION }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest
+120
View File
@@ -0,0 +1,120 @@
name: POEditor Translation Synchronization
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
jobs:
sync-translations:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
- 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: Install dependencies
run: |
cd .github/scripts
npm install
- 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: 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: 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: 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>
+90
View File
@@ -0,0 +1,90 @@
name: Demo deploy
on:
push:
branches: ["demo"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-demo
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-demo \
-f ./docker/prod/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-demo
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-demo \
-f ./docker/prod/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo
deploy-to-demo:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
steps:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docker/prod
docker compose down
docker compose pull
docker compose up -d
+91
View File
@@ -0,0 +1,91 @@
name: Staging deploy
on:
push:
branches: ["develop"]
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
-f ./docker/staging/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-staging
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-staging \
-f ./docker/staging/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-staging
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-staging \
-f ./docker/staging/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging
deploy-to-staging:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
steps:
- 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 }}
script: |
cd checkmate
git pull
cd docker/staging
docker compose down
docker compose pull
docker compose up -d
docker system prune -af
+113
View File
@@ -0,0 +1,113 @@
name: Upload Translations to POEditor on PR Merge
on:
pull_request:
types: [closed]
branches:
- develop
paths:
- "client/src/locales/**"
jobs:
upload-translations:
# Only run if the PR was merged (not just closed) or manually triggered
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history to get changed files
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
- 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",
"form-data": "^4.0.0"
}
}
EOF
- name: Install dependencies
run: |
cd .github/scripts
npm install
- name: Get changed locale files
id: changed-files
if: github.event_name == 'pull_request'
run: |
# Get base and head commits
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
echo "Base SHA: $BASE_SHA"
echo "Head SHA: $HEAD_SHA"
# Get list of changed files in client/src/locales directory
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA -- 'client/src/locales/*.json' || git ls-files 'client/src/locales/*.json')
if [ -z "$CHANGED_FILES" ]; then
echo "No changes detected in locale files"
echo "CHANGED_FILES=" >> $GITHUB_ENV
else
echo "Changed files:"
echo "$CHANGED_FILES"
echo "CHANGED_FILES<<EOF" >> $GITHUB_ENV
echo "$CHANGED_FILES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
fi
- name: Upload changed translations to POEditor
if: env.CHANGED_FILES != ''
env:
POEDITOR_API: ${{ secrets.POEDITOR_API }}
POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
run: |
# Process each changed file
for FILE in $CHANGED_FILES; do
if [[ -f "$FILE" ]]; then
# Extract language code from filename (e.g., client/src/locales/en.json -> en)
FILENAME=$(basename "$FILE")
# Special case: map gb.json to en language code
if [ "$FILENAME" == "gb.json" ]; then
LANG="en"
echo "Found gb.json, mapping to language code 'en'"
else
LANG=$(basename "$FILE" .json)
fi
echo "Processing $FILE for language $LANG"
# Upload to POEditor
LANGUAGE=$LANG FILE_PATH=$FILE node .github/scripts/upload-translations.js
fi
done
- name: Notify on success
if: success() && env.CHANGED_FILES != ''
run: |
echo "Successfully uploaded translation files to POEditor."
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Manual trigger comment: ${{ github.event.inputs.comment }}"
fi
- name: Notify on no changes
if: env.CHANGED_FILES == ''
run: |
echo "No translation files were found to upload."
+4 -6
View File
@@ -1,10 +1,8 @@
.idea
.vscode
.VSCodeCounter
AGENTS.md
*.sh
mongo
node_modules/
docs/architecture
docs/reviews
docs/todo
docs/frontend
notes
docker-compose.yml
mongo
-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# Skip during merge/rebase/cherry-pick/revert. lint-staged's internal
# git-stash mechanism is incompatible with these states and will silently
# wipe MERGE_HEAD and your conflict resolutions on commit failure.
git_dir=$(git rev-parse --git-dir)
if [ -e "$git_dir/MERGE_HEAD" ] \
|| [ -e "$git_dir/CHERRY_PICK_HEAD" ] \
|| [ -e "$git_dir/REVERT_HEAD" ] \
|| [ -d "$git_dir/rebase-merge" ] \
|| [ -d "$git_dir/rebase-apply" ]; then
echo "Skipping lint-staged (mid merge/rebase/cherry-pick/revert)"
exit 0
fi
# Anchor to repo root so commits made from a subdirectory still work, and
# run each package's lint-staged in a subshell so cwd is scoped (a failure
# inside one package can't poison the cwd for the next).
cd "$(git rev-parse --show-toplevel)" || exit 1
echo "Running lint-staged..."
(cd client && npx lint-staged) || exit 1
(cd server && npx lint-staged) || exit 1
-222
View File
@@ -1,222 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
## Development Commands
### Client (React/Vite)
```bash
cd client
npm install
npm run dev -- --port 10001 --strictPort # Local dev port is 10001 (5173 is used by another project on this machine)
npm run build # TypeScript check + production build
npm run lint # ESLint (strict, max-warnings 0)
npm run format # Prettier formatting
npm run format-check # Check formatting
```
Server `.env` on this machine is configured with `CLIENT_HOST="http://localhost:10001"` to match the client dev port. If you change the client port, keep `.env` in sync.
### Server (Node.js/Express)
```bash
cd server
npm install
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
npm run build # TypeScript compile + path alias resolution
npm run test # Run Jest tests with coverage
npm run lint # ESLint v9
npm run lint-fix # Auto-fix lint issues
npm run format # Prettier formatting
```
### Docker Development
```bash
docker build -f docker/Dockerfile -t checkmate . # build the mono image (server + built client)
docker compose -f docker/dev/docker-compose.yaml up # local full stack (builds image, runs mongo + worker)
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0 # just a dev database
```
## Environment Setup
### Server `.env` (minimum required)
```env
CLIENT_HOST="http://localhost:5173"
JWT_SECRET="my_secret_key_change_this"
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
TOKEN_TTL="99d"
ORIGIN="localhost"
LOG_LEVEL="debug"
```
### Client `.env`
```env
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
VITE_APP_LOG_LEVEL="debug"
```
## Architecture
### Monorepo Structure
- `/client` - React 18 + TypeScript + Vite + MUI frontend
- `/server` - Node.js 20+ + Express + TypeScript backend
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
### Backend Layers
```
server/src/
├── api/ # HTTP layer
│ ├── controllers/ # Route handlers (authController, monitorController, etc.)
│ ├── middleware/ # verifyJWT, rateLimiter, sanitization, responseHandler
│ ├── routes/ # API route definitions
│ └── validation/ # Zod request-payload schemas
├── domain/ # Business logic + data access, one folder per entity
│ ├── monitors/ # e.g. monitor.service.ts, monitor.repository.ts, monitor.type.ts
│ ├── checks/
│ ├── incidents/
│ └── … # users, teams, status-pages, notifications, etc.
├── service/ # Cross-cutting services (e.g. infrastructure/)
├── config/ # App config wiring (controllers, routes, services, envValidation)
├── db/
│ └── migration/ # Database migrations (run on startup)
├── templates/ # Email/notification templates
├── types/ # Shared const tuples + derived types
└── utils/ # Shared utilities
```
### Frontend Structure
```
client/src/
├── Components/ # Reusable UI components
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
├── Features/ # Redux slices (Auth, UI)
├── Hooks/ # Custom React hooks
├── Utils/ # Utilities (NetworkService.js is main API client)
├── Validation/ # Input validation
└── locales/ # i18n translations
```
### API
- Base URL: `/api/v1`
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
- OpenAPI spec: `/server/openapi.json`
### Key Technologies
- **State Management**: Redux Toolkit + Redux-Persist
- **Data Fetching**: SWR + Axios
- **Database**: MongoDB with Mongoose ODM
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
- **i18n**: i18next + react-i18next (non-English locales are machine translated, not managed through a translation service)
---
## 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
### Coding conventions (mandatory)
Read `docs/coding-conventions.md` before touching any `.tsx` or `.ts` file. The doc covers both frontend and backend rules, all enforced in code review.
**Universal — rule 0:** look at peer files first and follow the established pattern. Don't sneak in novel shapes.
**Frontend (`client/src`):**
1. Prefer MUI native props over `sx` (e.g. `color={…}`, `bgcolor={…}`, `mt={…}` — not `sx={{ color, bgcolor, mt }}`).
2. Use the full theme path for colors: `color={theme.palette.text.secondary}`, never `color="text.secondary"` (greppability).
3. No hardcoded literals — use `LAYOUT.*`, `typographyLevels.*`, `theme.shape.borderRadius`, `theme.palette.*`.
4. Use `useTheme()` inside components; don't `import { theme } from "@/Utils/Theme/Theme"`.
5. Pair runtime tuples with derived types: `const X = [...] as const; type X = (typeof X)[number]`.
**Backend (`server/src`):**
6. Layering: Controller → Service → Repository → Mongoose. No DB calls outside repositories.
7. Provider conventions: DI for stdlib clients, `SERVICE_NAME` + `TIMEOUT_MS` constants, `timeRequest()` helper, inline `setTimeout` race for timeout, outer try/catch into `AppError`.
8. Mongoose schema fields with closed value sets must declare `enum` reusing the same `types/*` const tuple.
9. Provider tests at `server/test/unit/providers/network/<name>.test.ts`, `from "@jest/globals"`, `testStatusProviderContract`, inline `setup()` per test (not `beforeEach`).
10. Centralize validation enums in `types/*` const tuples; never inline `z.enum([…])` more than once.
11. New entity fields must land in the validator's `*ResponseSchema` so the auto-generated OpenAPI spec sees them.
### Internationalization
All user-facing strings must use the translation function:
```javascript
t('your.key') // Never hardcode UI strings
```
### Branching
- Always branch from `develop` (not master)
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
- PRs target `develop` branch
### Formatting
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
- Both use ESLint with strict settings
### Testing
Server tests use Jest (with `--experimental-vm-modules` for ESM):
```bash
npm test # Run all tests with coverage
npm test -- -t "pattern" # Run tests matching name pattern
npm test -- path/to/file.test.ts # Run a specific file
```
Test files: `server/test/**/*.test.ts`
## Database Models
Key Mongoose models in `/server/src/db/models/`:
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
- **Check** - Individual monitoring check results
- **Incident** - Downtime incidents
- **User** - User accounts
- **Team** - Team/workspace management
- **StatusPage** - Public status pages
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
- **MaintenanceWindow** - Scheduled maintenance periods
- **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 **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!
# 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!
+30 -53
View File
@@ -10,31 +10,6 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
---
## 🚀 Quick Setup Checklist
Before you dive in, make sure you have these installed:
```bash
# Check Node.js (v20+ required)
node --version
# Check npm
npm --version
# Check Docker
docker --version
# Check Git
git --version
```
**New to contributing?** Start here:
1. Pick a [`good-first-issue`](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
2. Comment that you'd like to work on it
3. Follow the [setup guide](#set-up-checkmate-locally) below
4. Join our [Discord](https://discord.com/invite/NAb6H3UTjK) if you get stuck
## Table of contents
- [How do I...?](#how-do-i)
@@ -88,10 +63,28 @@ cd Checkmate
#### Step 2: Set Up Docker Containers (MongoDB)
Navigate to the Docker dev directory:
```bash
cd server/docker/dev
```
Build the Docker images:
```bash
./build_images.sh
```
Run MongoDB container:
```bash
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0
docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo
```
Navigate back to the root directory:
```bash
cd ../../..
```
#### Step 3: Set Up the Backend (Server)
@@ -127,7 +120,6 @@ LOG_LEVEL="debug"
- `ORIGIN`: Origin for CORS purposes
- `TOKEN_TTL`: Token time to live (in vercel/ms format)
- `LOG_LEVEL`: Debug level (debug, info, warn, error)
- `QUEUE_TYPE`: Job queue implementation (`superSimpleQueue` or `lessSimpleQueue`, optional, default: `superSimpleQueue`)
Start the backend server:
@@ -169,7 +161,7 @@ Start the frontend:
npm run dev
```
The client will run at `http://localhost:5173`.
The client will run at `http://localhost:5173`.
#### Step 5: Access the Application
@@ -217,7 +209,7 @@ docker rm uptime_database_mongo
**Need more help?**
- Check the [full documentation](https://checkmate.so/docs)
- Check the [full documentation](https://docs.checkmate.so)
- Ask on [Discord](https://discord.com/invite/NAb6H3UTjK)
### Start contributing code?
@@ -234,32 +226,17 @@ Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues
### Improve the documentation?
Documentation contributions are very welcome — fixing a typo, clarifying a step, or adding a missing example is a valuable PR.
- Small edits (typos, wording, formatting): open a PR directly against `develop`.
- Larger changes (restructuring a section, adding a new guide): open an issue first so we can align on scope, then a PR.
- Screenshots are helpful when documenting UI behaviour.
- README translations live in `docs/translations/`. If you're adding a new language, please also add a flag + link to the language bar in the root `README.md` and every existing translated README so they stay in sync.
Docs live in [checkmate-documentation](https://github.com/bluewave-labs/checkmate-documentation). You can fix typos, add guides, or explain features better.
### Help with translations?
The application uses `i18next` with one JSON file per language under `client/src/locales/` (`en.json` is the source of truth). English source strings are the definitive copy; other languages are generated as one-time machine translations that anyone is welcome to refine.
We use [PoEditor](https://poeditor.com) for translations. You can:
**To improve an existing language:**
- [Sign up and join your language team](https://poeditor.com/join/project/lRUoGZFCsJ).
- Translate UI strings.
- Ask questions on Discord in the relevant #translations channel.
1. Edit the relevant `client/src/locales/<code>.json` file directly.
2. Keep the JSON key structure identical to `en.json` — only translate the string values.
3. Open a PR against `develop`. No language-selector code changes are needed.
**To add a new language:**
1. Add `client/src/locales/<code>.json` with the same key structure as `en.json`, translated. If you can't translate every key, leave unfamiliar strings in English rather than removing keys — i18next falls back to the key path when a value is missing.
2. Add the display name to `languageNames` in `client/src/Components/inputs/LanguageSelector.tsx` (e.g. `it: "Italiano"`). The selector picks up any language present in `resources` automatically.
3. Open a PR against `develop`.
If translating a whole file is more than you want to take on, open an issue asking us to add the language and we'll generate an initial machine-translated file that you (or others) can refine over time.
**Note:** an older workflow that synced translations with PoEditor is still in the repo but no longer authoritative. Please treat the `.json` files in `client/src/locales/` as the source of truth.
Make sure all new UI strings in code use `t('key')`.
### Submit a pull request?
@@ -275,7 +252,7 @@ Follow the [pull request checklist](#pull-request-checklist). Your PR should:
## Code guidelines
- Use ESLint and Prettier. Run `npm run lint` and `npm run format-check` in both `client` and `server`. If `format-check` reports issues, fix them with `npm run format` before committing.
- Use ESLint and Prettier (`npm run lint`).
- Follow naming conventions: `camelCase` for variables, `PascalCase` for components, `UPPER_CASE` for constants.
- No hard-coded strings — use `t('your.key')` for everything visible.
- Use the shared theme and components. No magic numbers or hardcoded styles.
@@ -296,7 +273,7 @@ Before submitting your pull request, please confirm the following:
- You used the shared theme for any styling — no magic numbers or inline styles.
- The pull request addresses only one issue or topic.
- You added screenshots or a video for any UI-related changes.
- Your code passes `npm run lint`, `npm run format-check`, and `npm run build` in both `client` and `server` with no errors.
- Your code passes linting and has no TypeScript errors.
If one or more of these are missing, we may ask you to update your pull request before we can merge it.
@@ -308,7 +285,7 @@ If one or more of these are missing, we may ask you to update your pull request
- `master` is used for stable releases.
- Use descriptive branch names, like `fix/login-error` or `feat/add-alerts`.
- Make sure that you are using the latest version.
- Make sure you run the code locally. The Checkmate [documentation](https://checkmate.so/docs) covers it.
- Make sure you run the code locally. The Checkmate [documentation](https://docs.checkmate.so) covers it.
- Find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
+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 bug 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 big or work on another feature.
+172
View File
@@ -0,0 +1,172 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Mejores Prácticas](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![Pregunta en DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad</strong></p>
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados.
Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores.
Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento.
**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).**
## 📚 Tabla de contenidos
- [📦 Demo](#demo)
- [🔗 Guía del usuario](#guía-del-usuario)
- [🛠️ Instalación](#instalación)
- [🏁 Traducciones](#traducciones)
- [🚀 Rendimiento](#rendimiento)
- [💚 Preguntas e ideas](#preguntas-e-ideas)
- [🧩 Características](#características)
- [🏗️ Capturas de pantalla](#capturas-de-pantalla)
- [🏗️ Tecnologías](#tecnologías)
- [🔗 Enlaces útiles](#enlaces-útiles)
- [🤝 Contribuciones](#contribuciones)
- [💰 Patrocinadores](#patrocinadores)
...
**[Texto truncado para mantener la longitud del mensaje manejable]**
## Demo
Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones).
## Guía del usuario
Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :)
## Instalación
Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación.
## Traducciones
Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate.
## Rendimiento
Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## Preguntas e Ideas
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)
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
## Características
- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5)
- Monitoreo de sitios web
- Monitoreo de velocidad de carga
- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture)
- Monitoreo de Docker
- Monitoreo con ping
- Monitoreo de certificados SSL
- Monitoreo de puertos
- Incidentes en una sola vista
- Páginas de estado
- Notificaciones por correo, Webhooks, Discord, Telegram, Slack
- Mantenimiento programado
- Monitoreo de consultas JSON
- Soporte para múltiples idiomas
**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
- Mejores notificaciones
- Monitoreo de red
- ...y algunas funciones más
## Capturas de pantalla
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
## Tecnologías
- [ReactJs](https://react.dev/)
- [MUI (framework de React)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- ¡Y muchos otros componentes de código abierto!
## Enlaces útiles
- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch".
- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions).
- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos.
- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY)
## Contribuciones
Somos [Alex](http://github.com/ajhollid) (líder de equipo), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores.
Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo.
Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros!
Cómo contribuir:
0. Dale una estrella al repositorio :)
1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`.
2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura.
4. Abre un issue si crees que has encontrado un error.
5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo.
6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Historial de estrellas](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
## Patrocinadores
Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca
Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features).
También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners.
+50 -350
View File
@@ -1,350 +1,50 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
<p align="center">
🇺🇸 <a href="README.md">English</a> |
🇸🇦 <a href="docs/translations/README.ar.md">العربية</a> |
🇪🇸 <a href="docs/translations/README.ca.md">Català</a> |
🇨🇿 <a href="docs/translations/README.cs.md">Čeština</a> |
🇩🇪 <a href="docs/translations/README.de.md">Deutsch</a> |
🇪🇸 <a href="docs/translations/README.es.md">Español</a> |
🇫🇮 <a href="docs/translations/README.fi.md">Suomi</a> |
🇫🇷 <a href="docs/translations/README.fr.md">Français</a> |
🇮🇹 <a href="docs/translations/README.it.md">Italiano</a> |
🇯🇵 <a href="docs/translations/README.ja.md">日本語</a> |
🇧🇷 <a href="docs/translations/README.pt-BR.md">Português (Brasil)</a> |
🇷🇺 <a href="docs/translations/README.ru.md">Русский</a> |
🇹🇭 <a href="docs/translations/README.th.md">ไทย</a> |
🇹🇷 <a href="docs/translations/README.tr.md">Türkçe</a> |
🇺🇦 <a href="docs/translations/README.uk.md">Українська</a> |
🇻🇳 <a href="docs/translations/README.vi.md">Tiếng Việt</a> |
🇨🇳 <a href="docs/translations/README.zh-CN.md">简体中文</a> |
🇹🇼 <a href="docs/translations/README.zh-TW.md">繁體中文</a>
</p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong>
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate)
<img width="1461" height="853" alt="image" src="https://github.com/user-attachments/assets/d5ee1a4e-9db9-47cc-ad47-e1ba88a9d796" />
</p>
<br>
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.
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go.
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
## 📚 Table of contents
- [📦 Demo](#demo)
- [🔗 User's guide](#users-guide)
- [🛠️ Installation](#installation)
- [🚀 Performance](#performance)
- [💚 Questions & Ideas](#questions--ideas)
- [🧩 Features](#features)
- [🏗️ Screenshots](#screenshots)
- [🏗️ Tech stack](#tech-stack)
- [🔗 A few links](#a-few-links)
- [🤝 Contributing](#contributing)
<a id="demo"></a>
## Demo
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).
<a id="users-guide"></a>
## User's guide
Usage instructions can be found [here](https://checkmate.so/docs).
## Prerequisites
- [Docker](https://www.docker.com/) installed
- [Git](https://git-scm.com/) installed
<a id="installation"></a>
## Installation
The quickest way to run Checkmate is the reference Docker Compose file. It starts two services: the all-in-one Checkmate application image (`ghcr.io/bluewave-labs/checkmate`) and a separate MongoDB service.
> **What “all-in-one” means:** the Checkmate application is packaged in a single image; MongoDB is not embedded in that image and remains required. The reference Compose file starts MongoDB for you. For custom deployments, configure `DB_CONNECTION_STRING` to use an external MongoDB instance.
```bash
curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.yaml
JWT_SECRET="$(openssl rand -hex 32)" docker compose up -d
```
Then open http://localhost:52345. If the app is reached at another origin (domain or LAN IP), set `CLIENT_HOST` accordingly. To build the image yourself, run `docker build -f docker/Dockerfile -t checkmate .` from a checkout. For TLS, put any reverse proxy (Caddy, Traefik, nginx) in front of port 52345.
There are also 1-click installation options like [Repocloud](https://repocloud.io/details/Checkmate),
[Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) or [Cloudzy](https://cloudzy.com/marketplace/checkmate). Note that the Helm chart has not yet been migrated to the all-in-one image: it still deploys the legacy `checkmate-client`, `checkmate-backend` and `checkmate-mongo` images, pinned at v3.8.1.
### Configuration
The image is configured entirely through environment variables on the server container:
| Variable | Required | Description |
|---|---|---|
| `DB_CONNECTION_STRING` | Yes | MongoDB connection string, e.g. `mongodb://mongodb:27017/uptime_db` |
| `JWT_SECRET` | Yes | Secret used to sign auth tokens; generate one with `openssl rand -hex 32` |
| `CLIENT_HOST` | Yes | The URL users reach the app at, e.g. `https://checkmate.example.com`; used for CORS and for links in notifications and emails |
| `ENCRYPTION_KEY` | No | Encrypts stored Docker TLS client keys at rest; generate one with `openssl rand -base64 32`. Comma-separated list: the first key encrypts, every key decrypts. Must be identical on the API and every worker. To rotate without downtime, deploy `OLD_KEY,NEW_KEY` everywhere, then `NEW_KEY,OLD_KEY` everywhere, wait for the worker to re-encrypt every row, then drop `OLD_KEY`. |
| `PORT` | No | Port the API and web client are served on (default `52345`) |
| `HEALTH_PORT` | No | Port for the `/livez`, `/readyz` and `/metrics` endpoints, served by any process that runs the job worker (default `52346`) |
| `NODE_ENV` | No | `development`, `production` or `test` (default `development`). `development` disables the general API rate limiter; set `production` on real deployments |
| `LOG_LEVEL` | No | Server log level: `error`, `warn`, `info`, or `debug` (default `debug`) |
| `TOKEN_TTL` | No | Lifetime of issued auth tokens, e.g. `12h` or `7d` (default `99d`) |
| `QUEUE_MODE` | No | `primary` (default) runs the API, the web client and the job scheduler; `worker` runs a job-processing worker only, with no API |
| `QUEUE_PRIMARY_PROCESSES` | No | `true` (default) or `false`. Whether a `primary` node also processes monitoring jobs itself; set `false` when dedicated `worker` nodes handle all checks. Ignored in `worker` mode |
| `STATUS_PAGE_THEMES_ENABLED` | No | `true` (default) or `false`. When `false`, status pages ignore theme settings and always render the default theme |
The web client needs no configuration by default: it calls the API on the same origin it was served from (`/api/v1`). For setups where the defaults don't apply — for example, the API is reached through a different origin than the page — the server renders overrides into the client at runtime via these optional variables:
| Variable | Description |
|---|---|
| `CLIENT_CONFIG_API_BASE_URL` | Full base URL the client calls the API at, e.g. `https://api.example.com/api/v1`; defaults to same-origin `/api/v1` |
| `CLIENT_CONFIG_CLIENT_HOST` | Origin used when the client builds absolute links (invites, status pages); defaults to the browser's current origin |
| `CLIENT_CONFIG_LOG_LEVEL` | Browser console log level: `error`, `warn`, `info`, or `debug` (default `error`) |
> **Upgrading from an older image?** The `UPTIME_APP_*` variables (`UPTIME_APP_API_BASE_URL`, `UPTIME_APP_CLIENT_HOST`, `UPTIME_APP_LOG_LEVEL`) are no longer read. In most setups no replacement is needed — the same-origin defaults cover them; if you pointed the client at a different origin, use the `CLIENT_CONFIG_*` equivalents above. The `checkmate-client`, `checkmate-backend`, `checkmate-mongo`, and `checkmate-backend-mono-multiarch` images are no longer updated — switch to `ghcr.io/bluewave-labs/checkmate`, keeping your existing MongoDB service and data volume.
See full installation instructions in the [Checkmate documentation portal](https://checkmate.so/docs).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) (legacy images, pinned at v3.8.1), [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
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options.
### Docker monitors
A Docker monitor connects to a Docker daemon and reports on every container it runs. The daemon's ping response decides whether the monitor is up or down and its latency is the response time. Each check also records every container's state, health, CPU and memory usage, restart count, published ports and mounts. Enabling **Collect container logs** additionally stores the latest 200 log lines per container on every check; logs are kept for 7 days.
The **Docker host** field accepts two forms:
| Host | Example | Notes |
|---|---|---|
| Local socket | `unix:///var/run/docker.sock` | Also accepts a bare absolute path such as `/var/run/docker.sock`. Use this for the daemon on the same machine Checkmate runs on. |
| Remote daemon | `tcp://docker.example.com:2376` | Always uses mutual TLS; the port defaults to `2376`. Unencrypted daemons on `2375` are not supported. |
**Monitoring the local socket.** The reference Compose file does not mount the socket, so add it and grant the container the host's `docker` group. The image runs as an unprivileged user and cannot read the socket otherwise. Find the group id with `stat -c %g /var/run/docker.sock`, then:
```yaml
services:
checkmate:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
group_add:
- "989" # the gid printed by stat
```
**Monitoring a remote daemon.** Set the host to `tcp://host:port` and fill in the **TLS credentials** section of the monitor form with the same PEM files you would pass to `docker --tlsverify`:
- **CA certificate**: the CA that signed the daemon's server certificate. Required unless **Ignore TLS/SSL errors** is on, which skips verifying the daemon's identity.
- **Client certificate**: the certificate the daemon uses to authenticate Checkmate.
- **Client key**: the matching private key, unencrypted (no passphrase). Checkmate checks that it matches the certificate, encrypts it with `ENCRYPTION_KEY`, and never shows it again. Leave the field blank when editing to keep the stored key.
TLS Docker monitors require `ENCRYPTION_KEY` to be set on the server (see [Configuration](#configuration)). Saving one without it fails with an error telling you so. If the key is ever removed or rotated incorrectly, affected checks fail with a decryption error until it is restored.
If the daemon's certificate is signed by a private CA that you also want the rest of Checkmate to trust, see the [Custom CA Trust Guide](./docs/custom-ca-trust.md); for Docker monitors alone, the **CA certificate** field is enough.
### Securing the Docker daemon with TLS
The Docker daemon does not enable TLS by default. Follow these steps on the Docker host to generate a CA, a server certificate, and a client certificate for Checkmate. This is the procedure from [Docker's own guide](https://docs.docker.com/engine/security/protect-access/), condensed. Replace `docker.example.com` and `203.0.113.10` with your daemon's DNS name and IP.
**1. Create a CA.** The CA key gets a passphrase; keep it offline once the certificates are issued.
```bash
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj "/CN=docker-ca" -out ca.pem
```
**2. Create the server certificate.** The subject alternative names must cover every name or address Checkmate will use to reach the daemon.
```bash
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=docker.example.com" -sha256 -new -key server-key.pem -out server.csr
cat > server-ext.cnf <<EOF
subjectAltName = DNS:docker.example.com,IP:203.0.113.10
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out server-cert.pem -extfile server-ext.cnf
```
**3. Create the client certificate for Checkmate.** Do not add a passphrase to this key; Checkmate cannot use encrypted private keys.
```bash
openssl genrsa -out key.pem 4096
openssl req -subj "/CN=checkmate" -new -key key.pem -out client.csr
echo "extendedKeyUsage = clientAuth" > client-ext.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out cert.pem -extfile client-ext.cnf
rm client.csr server.csr server-ext.cnf client-ext.cnf
chmod 0400 ca-key.pem key.pem server-key.pem
chmod 0444 ca.pem server-cert.pem cert.pem
```
**4. Point the daemon at the certificates.** Move `ca.pem`, `server-cert.pem` and `server-key.pem` to `/etc/docker/certs/` and configure `/etc/docker/daemon.json`:
```json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem"
}
```
On distributions where the systemd unit already passes `-H fd://` (Debian, Ubuntu and derivatives), the daemon refuses to start with `hosts` set in both places. Remove the flag from the unit with an override, then restart:
```bash
sudo systemctl edit docker.service
```
```ini
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
```
```bash
sudo systemctl daemon-reload && sudo systemctl restart docker
```
Open port `2376` on the host firewall only to the machine running Checkmate.
**5. Verify from the Checkmate host**, then paste `ca.pem`, `cert.pem` and `key.pem` into the monitor form:
```bash
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
-H=docker.example.com:2376 version
```
For more documentation, see the [docs directory](./docs/).
<a id="performance"></a>
## 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:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
You can see the memory footprint of MongoDB on the same server (398Mb) for the same amount of servers:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
<a id="questions--ideas"></a>
## Questions & Ideas
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/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!
<a id="features"></a>
## Features
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
- Several monitoring options: HTTP (with SSL certificate expiry), Ping, Port, DNS, Docker, gRPC, WebSocket, Game server
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
- Selective disk monitoring with mountpoint selection
- Incidents at a glance
- Status pages with 5 beautiful themes
- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Rocket.Chat, Microsoft Teams, Telegram, Pushover, ntfy, SignalGrid, Twilio (SMS) notifications
- Scheduled maintenance
- JSON query monitoring
- Multi-language support for Arabic, Catalan, Chinese (Simplified), Chinese (Traditional, Taiwan), Czech, English, Finnish, French, German, Italian, Japanese, Polish, Portuguese (Brazil), Russian, Spanish, Thai, Turkish, Ukrainian, and Vietnamese
## 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
<a id="screenshots"></a>
## Screenshots
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
<a id="tech-stack"></a>
## Tech stack
- [ReactJs](https://react.dev/)
- [MUI (React framework)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- Lots of other open source components!
<a id="a-few-links"></a>
## A few links
- If you would like to support us, please consider giving it a ⭐ and click on "watch".
- Have a question or suggestion for the roadmap/featureset? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.
- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.
- Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY)
<a id="contributing"></a>
## Contributing
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Malena](https://github.com/malenacaroline) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned almost 11K stars and attracted 150+ contributors from around the globe.
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
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. 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/Checkmate&Date)
# Checkmate
A full‑stack uptime and infrastructure monitoring app. The repository contains an Express/TypeScript API and a React/TypeScript client.
## Project Structure
- `server/` – Node.js API (TypeScript, ESM)
- Source: `server/src`
- Routes mounted under `/api/v1/*` via `server/src/init/routes.ts`
- Tests: `server/tests`
- Build output: `server/dist`
- `client/` – React + TypeScript (Vite)
- Source: `client/src`
- Public assets: `client/public`
## Key Features
- JWT authentication and permission checks (team/org).
- Monitors API: create, list (with sorting/pagination), get checks, toggle pause, update, delete.
- Import/Export monitors (`GET /api/v1/monitors/export`, `POST /api/v1/monitors/import`).
- Notification channels and diagnostics endpoints.
- System settings (SMTP) with env‑first email transport fallback to DB settings.
## Getting Started
1. Prerequisites: Node 18+, MongoDB
2. Environment
- Server: create `server/.env` with at least:
- `MONGODB_URI=mongodb://localhost:27017/saas`
- `JWT_SECRET=replace_me`
- `ORIGIN=http://localhost:5173`
- Optional SMTP: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`
3. Install
- `cd server && npm ci`
- `cd ../client && npm ci`
4. Run
- API: `cd server && npm run dev` (listens on `http://localhost:52345`)
- Client: `cd client && npm run dev` (Vite dev server)
## Scripts
- Server
- `npm run dev` – Start API in watch mode
- `npm run build` – Type‑check and compile (tsc + tsc‑alias)
- `npm test` / `npm run test:coverage` – Jest tests and coverage
- Client
- `npm run dev` – Start Vite dev server
- `npm run build` – Type‑check and build
- `npm run preview` – Preview production build
## Notes
- The API expects valid JWTs on most routes; see `server/src/routes` for middleware usage.
- Mongo connection retries are handled in `server/src/db/MongoDB.ts`.
- Email transport prioritizes env config; if unset, it uses values from system settings.
-17
View File
@@ -1,17 +0,0 @@
.DS_Store
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
-10
View File
@@ -1,10 +0,0 @@
apiVersion: v2
name: checkmate-chart
description: A Helm chart for Checkmate App
type: application
version: 0.2.0
# DO NOT bump appVersion past v3.8.1 until this chart is migrated to the all-in-one
# image (ghcr.io/bluewave-labs/checkmate). The checkmate-client / checkmate-backend /
# checkmate-mongo images this chart references are frozen at v3.8.1 — later versions
# are never published, so a bumped chart will fail to pull.
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
-246
View File
@@ -1,246 +0,0 @@
# Kubernetes Installation Guide for Checkmate
This guide walks you through deploying Checkmate on your Kubernetes cluster using Helm.
## Prerequisites
- A running Kubernetes cluster
- Helm CLI installed and configured
- `kubectl` configured to access your cluster
## Steps
### 1. Clone the repo and navigate to the Helm chart
```bash
git clone https://github.com/bluewave-labs/checkmate.git
cd checkmate/charts/helm/checkmate
```
### 2. Customize values.yaml
Edit `values.yaml` to update:
- `client.ingress.host` and `api.ingress.host` with your domain names
- `api.protocol` (usually http or https)
- **If upgrading**: Migrate persistence settings from flat structure to nested:
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
- Add: `persistence.mongo.storageClass` (leave empty for default)
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
### 3. Deploy the Helm chart
```bash
helm install checkmate ./charts/helm/checkmate
```
This will deploy the client, api, and MongoDB components.
### 4. Verify the deployment
Check pods and services:
```bash
kubectl get pods
kubectl get svc
```
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
## Upgrading from a previous release
This release renames the `server.*` value block to `api.*` and drops the (unused) bundled Redis.
Read this before running `helm upgrade` on an existing install.
### `server.*` → `api.*` (values rename)
The value block formerly named `server` is now `api` (`server.image` → `api.image`,
`server.ingress.host` → `api.ingress.host`, `server.protocol` → `api.protocol`, `server.resources`,
`server.affinity`, `server.tolerations`, `server.ingress.*`, etc.).
**Your existing `server.*` overrides keep working** — the chart merges any legacy `server.*` values on
top of the `api.*` defaults (legacy wins where set), so an unchanged values file upgrades cleanly. The
shim is transitional; migrate your overrides to `api.*` at your convenience and drop the `server:`
block. If you set **both** `server.*` and `api.*` for the same field, the legacy `server.*` value wins —
don't mix them.
### Resource renames cause a replace, not a rename
The Kubernetes objects are renamed alongside the values:
| Old object | New object |
| --- | --- |
| Deployment/Service `checkmate-server` | `checkmate-api` |
| Ingress `checkmate-server-ingress` | `checkmate-api-ingress` |
| ConfigMap `checkmate-server-nginx-cm` | `checkmate-client-nginx-cm` |
Helm deletes the old objects and creates the new ones on upgrade. Expect a brief API restart and a
**new ClusterIP** for the API Service — update anything outside the chart that referenced the
`checkmate-server` Service by name. MongoDB data (its PVC) is untouched.
### Redis removed
The bundled Redis StatefulSet/Service and the `redis.*` / `persistence.redis.*` values are gone — the
scheduler runs in-process and never used them. Default installs (`redis.enabled: false`) are
unaffected. If you had explicitly set `redis.enabled: true`, **the Redis StatefulSet and its PVC are
deleted on upgrade** — back up anything you stored there first. Leftover `redis:` / `persistence.redis:`
keys in your values file are harmless (ignored).
### New secret defaults
`NODE_ENV: production`, `LOG_LEVEL: info`, and `TOKEN_TTL: 99d` are now part of the default `secrets`
block and merge into your install unless you override them. Set them explicitly if you relied on
different values (e.g. a non-production `NODE_ENV`).
### Image tags follow the chart's `appVersion`
Each tier's `image` value is now the repository **without a tag**; the tag defaults to the chart's
`appVersion`, so the client, api, worker, and mongodb images all move together when you upgrade the
chart. You can still pin per tier:
```yaml
api:
image: ghcr.io/bluewave-labs/checkmate-backend # repo only
tag: "" # empty → Chart.appVersion; set e.g. "v3.1.0" to pin, or "latest" for a dev tag
```
- `<tier>.tag` overrides the version for that tier. The `worker` tier inherits `api.tag` (and
`api.image`) unless you set `worker.tag` / `worker.image`.
- If you put a **full `repo:tag`** in `image` (e.g. a legacy `server.image: …:v3.2.0` override), it is
used verbatim and the `tag` field is ignored — so existing pinned values keep working unchanged.
**Prefer a pinned tag over `latest`.** With the default `imagePullPolicy: IfNotPresent`, `latest` is
cached per node and never refreshed, and `helm rollback` can't move a floating tag — pin a version (or
let it default to `appVersion`) for reproducible, rollback-able deploys.
## Scaling: the worker tier & autoscaling
By default the chart runs a single all-in-one API pod that both schedules **and** processes monitoring
jobs (`worker.enabled: false`). For larger deployments you can split processing onto a dedicated,
horizontally-scalable worker tier.
### Topology
| `worker.enabled` | API (`checkmate-api`) | Worker (`checkmate-worker`) |
| --- | --- | --- |
| `false` (default) | schedules **and** processes jobs (`QUEUE_PRIMARY_PROCESSES=true`) | not deployed |
| `true` | schedules + serves the API only (`QUEUE_PRIMARY_PROCESSES=false`, derived) | processes all jobs |
`QUEUE_PRIMARY_PROCESSES` is **derived** from `worker.enabled` — you never set it directly, so the API
and worker tier can never both process (or both ignore) the queue.
### Enabling the worker tier
```yaml
worker:
enabled: true
replicas: 2 # used only when autoscaling.enabled = false
```
The worker takes **no inbound traffic** (no Service): the kubelet probes pod IPs directly on
`HEALTH_PORT` (`/livez`, `/readyz`), and graceful shutdown drains in-flight jobs before exit.
> **Invariant:** `worker.terminationGracePeriodSeconds` (default `30`) **must stay greater than the
> server drain timeout (25s)**. Lower it and Kubernetes SIGKILLs workers mid-drain, losing in-flight
> checks.
> **Caveat:** with the worker tier enabled the API no longer processes jobs, so if **all** workers are
> down nothing processes. The `autoscaling.minReplicaCount: 1` floor plus liveness restarts mitigate
> this; full failover to the API would need leader election (not supported).
### Autoscaling with KEDA
Backlog-driven autoscaling is **off by default** and requires the
[KEDA operator](https://keda.sh/) installed in-cluster. Enable it with:
```yaml
worker:
enabled: true
autoscaling:
enabled: true
minReplicaCount: 1 # processing floor — never 0, or checks silently stall
maxReplicaCount: 10
backlogPerReplica: 50 # target due-job backlog per worker
mongo:
dbName: uptime_db # MUST match the database in DB_CONNECTION_STRING
```
A KEDA `ScaledObject` queries MongoDB directly for the due-check backlog and scales the worker
Deployment between `minReplicaCount` and `maxReplicaCount`. When autoscaling is on, the Deployment
omits `replicas` so it doesn't fight the HPA KEDA creates. Make sure `autoscaling.mongo.dbName`
matches the database name in your `DB_CONNECTION_STRING`, or the scaler counts the wrong DB (always 0
backlog → never scales up).
#### Fallback: Prometheus-adapter HPA (not shipped)
If your KEDA/Mongo versions reject the `$$NOW`/`$toLong` query, or you already run Prometheus, scrape
the worker's `/metrics` (`checkmate_worker_due_backlog`), expose it via prometheus-adapter, and target
it with a standard `HorizontalPodAutoscaler`. The manifest is left to the operator; the app computes
`now` in code on this path, so the query coercion is unnecessary.
## Enabling TLS/HTTPS with cert-manager
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
### Prerequisites
- cert-manager installed in your cluster
- A ClusterIssuer or Issuer configured (e.g., `letsencrypt-prod`)
### Configuration
Edit `values.yaml` to enable TLS (and update protocols to https):
```yaml
client:
protocol: https
ingress:
enabled: true
host: checkmate.example.com
className: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-client-tls
api:
protocol: https
ingress:
enabled: true
host: checkmate.example.com
className: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-api-tls
```
### Alternative: Using --set flags
You can also enable TLS during installation using Helm's `--set` flags:
```bash
helm install checkmate ./charts/helm/checkmate \
--set client.protocol=https \
--set api.protocol=https \
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set client.ingress.tls.enabled=true \
--set client.ingress.tls.secretName=checkmate-client-tls \
--set api.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set api.ingress.tls.enabled=true \
--set api.ingress.tls.secretName=checkmate-api-tls
```
### Verification
After deployment, cert-manager will automatically create the TLS secrets. You can verify the certificate status:
```bash
# Check certificates
kubectl get certificates
# Check certificate details
kubectl describe certificate checkmate-client-tls
kubectl describe certificate checkmate-api-tls
# Verify the secrets were created
kubectl get secrets | grep checkmate-tls
```
The ingress will automatically use these secrets to enable HTTPS access to your Checkmate instance.
@@ -1,35 +0,0 @@
{{- /*
Back-compat shim: the `server.*` value block was renamed to `api.*`. Existing installs that
still pass a legacy `server:` block keep working — this returns the effective API values with any
legacy `server.*` overrides merged on top of the `api.*` defaults (legacy wins where set).
New installs have no `server:` key, so this is a no-op deepCopy of `.Values.api`. Consume it as:
{{- $api := include "checkmate.api" . | fromYaml -}}
then reference `$api.*` instead of `.Values.api.*`.
*/}}
{{- define "checkmate.api" -}}
{{- $api := deepCopy .Values.api -}}
{{- with .Values.server -}}
{{- $api = mergeOverwrite $api (deepCopy .) -}}
{{- end -}}
{{- $api | toYaml -}}
{{- end -}}
{{- /*
Resolve a full image reference from a repo + optional tag. `appVersion` is the single source of
truth for tags: bumping Chart.appVersion moves every tier at once. Call as:
{{ include "checkmate.image" (dict "image" $repo "tag" $tag "root" $) }}
If `image` already carries a tag (a ":" in its final path segment) it is used verbatim — this keeps
legacy `server.image: repo:tag` overrides working and lets anyone pin a full ref (e.g. tag "latest").
A ":" only in the registry segment (a registry port like "reg:5000/x") is ignored, so a tag is still
appended. Otherwise the tag is `tag` if set, else the chart's appVersion.
*/}}
{{- define "checkmate.image" -}}
{{- $lastSegment := .image | splitList "/" | last -}}
{{- if contains ":" $lastSegment -}}
{{- .image -}}
{{- else -}}
{{- printf "%s:%s" .image (.tag | default .root.Chart.AppVersion) -}}
{{- end -}}
{{- end -}}
@@ -1,65 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-api
spec:
replicas: 1 # single scheduler — seeds/reconciles the jobs collection
selector:
matchLabels:
app: checkmate-api
template:
metadata:
labels:
app: checkmate-api
spec:
{{- with $api.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with $api.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: api
image: {{ include "checkmate.image" (dict "image" $api.image "tag" $api.tag "root" $) }}
imagePullPolicy: {{ $api.imagePullPolicy }}
ports:
- name: http
containerPort: {{ $api.port }}
envFrom:
- secretRef:
name: checkmate-secrets
# Probes hit the Express /api/v1/health endpoint — proves the HTTP server is responsive.
# NOTE: this does NOT detect a wedged job loop when queuePrimaryProcesses=true (the API is
# also a worker). Worker-aware health for that mode is a follow-up — see worker-health-checks.md §7.5.
livenessProbe:
httpGet:
path: /api/v1/health
port: http
initialDelaySeconds: {{ $api.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ $api.probes.liveness.periodSeconds }}
timeoutSeconds: {{ $api.probes.liveness.timeoutSeconds }}
failureThreshold: {{ $api.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: /api/v1/health
port: http
initialDelaySeconds: {{ $api.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ $api.probes.readiness.periodSeconds }}
timeoutSeconds: {{ $api.probes.readiness.timeoutSeconds }}
failureThreshold: {{ $api.probes.readiness.failureThreshold }}
{{- with $api.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
env:
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
- name: QUEUE_MODE
value: "primary"
# Derived: when a worker tier is deployed the API only schedules (false); otherwise it is
# all-in-one and processes jobs itself (true). Keeps the two from ever competing for jobs.
- name: QUEUE_PRIMARY_PROCESSES
value: {{ not .Values.worker.enabled | quote }}
@@ -1,39 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if $api.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkmate-api-ingress
{{- if $api.ingress.annotations }}
annotations:
{{- range $key, $value := $api.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{/*#annotations:
#nginx.ingress.kubernetes.io/rewrite-target: /
#nginx.ingress.kubernetes.io/enable-cors: "true"
#nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
#nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
#nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
spec:
ingressClassName: {{ $api.ingress.className }}
{{- if $api.ingress.tls.enabled }}
tls:
- hosts:
- {{ $api.ingress.host }}
secretName: {{ default (printf "%s-api-tls" .Release.Name) $api.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ $api.ingress.host }}
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: checkmate-api
port:
number: {{ $api.port }}
{{- end }}
@@ -1,10 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: Service
metadata:
name: checkmate-api
spec:
selector:
app: checkmate-api
ports:
- port: {{ $api.port }}
@@ -1,46 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-client
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-client
template:
metadata:
labels:
app: checkmate-client
spec:
{{- with .Values.client.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.client.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: client
image: {{ include "checkmate.image" (dict "image" .Values.client.image "tag" .Values.client.tag "root" $) }}
imagePullPolicy: {{ .Values.client.imagePullPolicy }}
ports:
- containerPort: {{ .Values.client.port }}
env:
- name: UPTIME_APP_API_BASE_URL
value: "{{ $api.protocol }}://{{ $api.ingress.host }}/api/v1"
- name: UPTIME_APP_CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
{{- with .Values.client.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
volumes:
- name: config-volume
configMap:
name: checkmate-client-nginx-cm
@@ -1,31 +0,0 @@
{{- if .Values.client.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkmate-client-ingress
{{- if .Values.client.ingress.annotations }}
annotations:
{{- range $key, $value := .Values.client.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
spec:
ingressClassName: {{ .Values.client.ingress.className }}
{{- if .Values.client.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.client.ingress.host }}
secretName: {{ default (printf "%s-client-tls" .Release.Name) .Values.client.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.client.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: checkmate-client
port:
number: {{ .Values.client.port }}
{{- end }}
@@ -1,42 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: checkmate-client-nginx-cm
data:
default.conf: |
server {
listen 80;
listen [::]:80;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# location /api/ {
# proxy_pass http://{{ $api.ingress.host }}:5000/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://{{ $api.ingress.host }}:5000/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;
}
}
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: checkmate-client
spec:
selector:
app: checkmate-client
ports:
- port: {{ .Values.client.port }}
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: checkmate-mongodb
spec:
selector:
app: checkmate-mongodb
ports:
- port: {{ .Values.mongodb.port }}
@@ -1,47 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: checkmate-mongodb
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-mongodb
template:
metadata:
labels:
app: checkmate-mongodb
spec:
{{- with .Values.mongodb.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.mongodb.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: mongodb
image: {{ include "checkmate.image" (dict "image" .Values.mongodb.image "tag" .Values.mongodb.tag "root" $) }}
imagePullPolicy: {{ .Values.mongodb.imagePullPolicy }}
ports:
- containerPort: {{ .Values.mongodb.port }}
command: ["mongod", "--quiet", "--bind_ip_all"]
volumeMounts:
- name: checkmate-mongo-persistent-storage
mountPath: /data/db
{{- with .Values.mongodb.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
volumeClaimTemplates:
- metadata:
name: checkmate-mongo-persistent-storage
spec:
{{- with .Values.persistence.mongo.storageClass }}
storageClassName: {{ . | quote }} {{/* omitted when empty so the cluster's default StorageClass applies */}}
{{- end }}
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.persistence.mongo.size | default "5Gi" | quote }}
@@ -1,54 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if eq .Values.client.ingress.host "change_me" }}
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- if eq $api.ingress.host "change_me" }}
{{- fail "api.ingress.host (or legacy server.ingress.host) must be overridden and not set to 'change_me'" }}
{{- end }}
{{- $apiProtocol := $api.protocol }}
{{- if not (or (eq $apiProtocol "http") (eq $apiProtocol "https")) }}
{{- fail "api.protocol must be either 'http' or 'https'" }}
{{- end }}
{{- $clientProtocol := .Values.client.protocol }}
{{- if not (or (eq $clientProtocol "http") (eq $clientProtocol "https")) }}
{{- fail "client.protocol must be either 'http' or 'https'" }}
{{- end }}
{{/* Enforce protocol when TLS is enabled to avoid mixed-content */}}
{{- if and .Values.client.ingress.tls.enabled (ne $clientProtocol "https") }}
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
{{- end }}
{{- if and $api.ingress.tls.enabled (ne $apiProtocol "https") }}
{{- fail "api.ingress.tls.enabled is true but api.protocol is not 'https'. Set api.protocol: https to ensure correct API base URL." }}
{{- end }}
{{/* If client runs on https, API must also be https to avoid mixed content */}}
{{- if and (eq $clientProtocol "https") (ne $apiProtocol "https") }}
{{- fail "client.protocol is 'https' but api.protocol is not. Set api.protocol: https to prevent browser mixed-content issues." }}
{{- end }}
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
{{- $aAnn := $api.ingress.annotations | default dict }}
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
{{- $apiHasIssuer := or (hasKey $aAnn "cert-manager.io/cluster-issuer") (hasKey $aAnn "cert-manager.io/issuer") }}
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{- if and $api.ingress.tls.enabled (not $apiHasIssuer) }}
{{- fail "api.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{/* Worker grace period must exceed the server drain timeout (25s) or workers are SIGKILLed mid-drain */}}
{{- if and .Values.worker.enabled (le (int .Values.worker.terminationGracePeriodSeconds) 25) }}
{{- fail "worker.terminationGracePeriodSeconds must be greater than 25 (the server drain timeout) or workers are killed mid-drain" }}
{{- end }}
{{/* Secret name can be omitted; we default to <release>-client|api-tls in templates */}}
@@ -1,27 +0,0 @@
{{- $secrets := .Values.secrets }}
{{- if or (not $secrets.JWT_SECRET) (eq $secrets.JWT_SECRET "change_me") }}
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
{{- end }}
{{- /*
The bundled Mongo service ("checkmate-mongodb") lives in .Release.Namespace. App pods reach it
by bare name, but the KEDA operator runs in its own namespace and a bare name is NXDOMAIN there,
so worker autoscaling fails to query the backlog. Qualify the in-cluster host to its FQDN here so
the one connection string resolves from every namespace. External/already-qualified hosts are not
matched (the char after the host name is "." for an FQDN, which the pattern excludes).
*/}}
{{- $mongoFqdn := printf "checkmate-mongodb.%s.svc.cluster.local" .Release.Namespace }}
apiVersion: v1
kind: Secret
metadata:
name: checkmate-secrets
type: Opaque
stringData:
{{- range $key, $value := $secrets }}
{{- if eq $key "DB_CONNECTION_STRING" }}
{{ $key }}: {{ regexReplaceAll "(//|@)checkmate-mongodb([:/,]|$)" $value (printf "${1}%s${2}" $mongoFqdn) | quote }}
{{- else }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
@@ -1,66 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if .Values.worker.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-worker
spec:
{{- if not .Values.worker.autoscaling.enabled }}
replicas: {{ .Values.worker.replicas }} # omitted when autoscaling owns the replica count
{{- end }}
selector:
matchLabels:
app: checkmate-worker
template:
metadata:
labels:
app: checkmate-worker
spec:
terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }}
{{- with .Values.worker.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.worker.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: worker
image: {{ include "checkmate.image" (dict "image" (.Values.worker.image | default $api.image) "tag" (.Values.worker.tag | default $api.tag) "root" $) }}
imagePullPolicy: {{ .Values.worker.imagePullPolicy | default $api.imagePullPolicy }}
ports:
- name: health
containerPort: {{ .Values.worker.healthPort }}
envFrom:
- secretRef:
name: checkmate-secrets # DB_CONNECTION_STRING (+ JWT_SECRET/CLIENT_HOST for validateEnv)
env:
# Secret default CLIENT_HOST is the non-URL "change_me"; override like the API so validateEnv passes.
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
- name: QUEUE_MODE
value: "worker"
- name: HEALTH_PORT
value: {{ .Values.worker.healthPort | quote }}
livenessProbe:
httpGet:
path: /livez
port: health
initialDelaySeconds: {{ .Values.worker.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.worker.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.worker.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.worker.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: {{ .Values.worker.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.worker.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.worker.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.worker.probes.readiness.failureThreshold }}
{{- with .Values.worker.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
{{- end }}
@@ -1,30 +0,0 @@
{{- if and .Values.worker.enabled .Values.worker.autoscaling.enabled }}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: checkmate-worker
spec:
scaleTargetRef:
name: checkmate-worker
minReplicaCount: {{ .Values.worker.autoscaling.minReplicaCount }} # floor: never 0 — a cold gap stops checks
maxReplicaCount: {{ .Values.worker.autoscaling.maxReplicaCount }}
cooldownPeriod: {{ .Values.worker.autoscaling.cooldownPeriod }}
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: {{ .Values.worker.autoscaling.stabilizationWindowSeconds }}
triggers:
- type: mongodb
metadata:
dbName: {{ .Values.worker.autoscaling.mongo.dbName | quote }}
collection: {{ .Values.worker.autoscaling.mongo.collection | quote }}
# The Phase 2 countDueBacklog predicate, time-relativized with $$NOW (epoch-ms via $toLong):
query: >-
{ "type": "check", "isActive": true,
"$expr": { "$lte": ["$nextScheduledAt", { "$toLong": "$$NOW" }] },
"$or": [ { "lockedUntil": null },
{ "$expr": { "$lt": ["$lockedUntil", { "$toLong": "$$NOW" }] } } ] }
queryValue: {{ .Values.worker.autoscaling.backlogPerReplica | quote }}
connectionStringFromEnv: DB_CONNECTION_STRING # resolved from the worker's checkmate-secrets
{{- end }}
-108
View File
@@ -1,108 +0,0 @@
client:
image: ghcr.io/bluewave-labs/checkmate-client
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
port: 80
protocol: http
ingress:
enabled: true
host: change_me
className: nginx
annotations: {}
# Example annotations for cert-manager:
# annotations:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-client-tls # Optional; defaults to <release>-client-tls if omitted
# Note: when enabling TLS, also set client.protocol: https and add
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
api:
image: ghcr.io/bluewave-labs/checkmate-backend
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
port: 52345
protocol: http
# Liveness/readiness probes hit GET /api/v1/health on the api port (proves the HTTP server is up).
probes:
liveness:
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readiness:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
ingress:
enabled: true
host: change_me
className: nginx
annotations: {}
# Example annotations for cert-manager:
# annotations:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-api-tls # Optional; defaults to <release>-api-tls if omitted
# Note: when enabling TLS, also set api.protocol: https and add
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
mongodb:
image: ghcr.io/bluewave-labs/checkmate-mongo
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
port: 27017
worker:
# Off by default; existing installs are unchanged. When true, a dedicated worker processes all
# jobs and the API stops processing (QUEUE_PRIMARY_PROCESSES is derived as false). When false, the
# API is all-in-one and processes jobs itself.
enabled: false
image: "" # defaults to api.image when empty, same as API image
tag: "" # defaults to api.tag (-> Chart.appVersion) when empty
imagePullPolicy: "" # defaults to api.imagePullPolicy when empty
replicas: 1 # used only when autoscaling.enabled = false
healthPort: 52346 # HEALTH_PORT; serves /livez /readyz /metrics
terminationGracePeriodSeconds: 30 # MUST stay > the server drain timeout (25s) or workers are killed mid-drain
resources: {}
affinity: {}
tolerations: {}
probes:
liveness: # /livez — failureThreshold>=3 so one slow tick never restarts mid-job
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readiness: # /readyz — flips to 503 on drain
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
autoscaling:
enabled: false # requires the KEDA operator installed in-cluster
minReplicaCount: 1 # processing floor — never 0 (a cold gap silently stops checks)
maxReplicaCount: 10
backlogPerReplica: 50 # KEDA queryValue: target due-backlog per worker
cooldownPeriod: 300 # seconds before scaling an idle target down
stabilizationWindowSeconds: 300 # damp the interval-boundary herd
mongo:
dbName: uptime_db # must match DB_CONNECTION_STRING's database
collection: jobs
secrets:
JWT_SECRET: change_me
DB_CONNECTION_STRING: mongodb://checkmate-mongodb:27017/uptime_db
CLIENT_HOST: change_me
NODE_ENV: production
LOG_LEVEL: info
TOKEN_TTL: 99d
persistence:
mongo:
size: 5Gi
storageClass: ""
-14
View File
@@ -1,14 +0,0 @@
language: "en-CA"
tone_instructions: "His palms are sweaty, knees weak, arms are heavy There’s vomit on his sweater already, mom’s spaghetti"
early_access: false
reviews:
profile: "chill"
request_changes_workflow: false
high_level_summary: false
poem: false
review_status: false
auto_review:
enabled: true
drafts: false
chat:
auto_reply: false
-3
View File
@@ -1,3 +0,0 @@
Client/node_modules/
Server/node_modules/
Server/*.env
-6
View File
@@ -1,6 +0,0 @@
# Production builds use same-origin defaults: API at /api/v1, client host from
# window.location.origin. The empty values below override any local .env so a
# production build never bakes in a developer's localhost URLs.
VITE_APP_API_BASE_URL=
VITE_APP_CLIENT_HOST=
VITE_APP_LOG_LEVEL=
-158
View File
@@ -1,158 +0,0 @@
// MUI sx keys that have a native (system) prop equivalent.
// Flagged when found inside an sx={{ ... }} object literal — see frontend-conventions.md rule #1.
const SX_NATIVE_EQUIVALENT_KEYS = [
"color",
"bgcolor",
"backgroundColor",
"borderRadius",
"border",
"borderTop",
"borderBottom",
"borderLeft",
"borderRight",
"width",
"height",
"minHeight",
"maxWidth",
"minWidth",
"maxHeight",
"p",
"pt",
"pb",
"px",
"py",
"pl",
"pr",
"m",
"mt",
"mb",
"mx",
"my",
"ml",
"mr",
"display",
"textAlign",
"fontWeight",
"fontSize",
"lineHeight",
"gap",
];
// MUI components that accept system props as top-level attributes.
// sx={{ color, p, mt, ... }} on these can be hoisted into native props.
// Dialog*, Drawer*, Menu*, Tab*, Toolbar etc. only accept sx — don't flag those.
const SX_HOISTABLE_COMPONENTS = [
"Box",
"Stack",
"Typography",
"Grid",
"Container",
"Paper",
"Card",
"CardContent",
"CardActions",
"CardHeader",
"Divider",
"Link",
"Chip",
];
const sxKeySelector = SX_HOISTABLE_COMPONENTS.flatMap((component) =>
SX_NATIVE_EQUIVALENT_KEYS.map(
(k) =>
`JSXOpeningElement[name.name='${component}'] > JSXAttribute[name.name='sx'] > JSXExpressionContainer > ObjectExpression > Property[key.name='${k}']`
)
).join(", ");
const FRONTEND_CONVENTION_RULES = {
// Rule #2 — color/bgcolor/borderColor as a string shorthand like "text.secondary".
// Catches: <Typography color="text.secondary" /> — should be color={theme.palette.text.secondary}.
"no-restricted-syntax": [
"warn",
{
selector:
"JSXAttribute[name.name=/^(color|bgcolor|borderColor)$/] > Literal[value=/\\./]",
message:
'Use the full theme path instead of a string shorthand. e.g. color={theme.palette.text.secondary} not color="text.secondary". See docs/frontend-conventions.md rule #2.',
},
{
// Rule #1 — sx={{ key: ... }} where key has a native prop equivalent.
selector: sxKeySelector,
message:
"Hoist this key out of `sx` onto the component as a native MUI prop (e.g. color={...}, mt={...}, width={...}). See docs/frontend-conventions.md rule #1.",
},
{
// Rule #4 — importing the singleton theme from the Theme module.
// Catches: import { theme } from "@/Utils/Theme/Theme" — should be useTheme() inside the component.
selector:
"ImportDeclaration[source.value='@/Utils/Theme/Theme'] > ImportSpecifier[imported.name='theme']",
message:
"Don't import { theme } from @/Utils/Theme/Theme — use the useTheme() hook inside the component. See docs/frontend-conventions.md rule #4.",
},
{
// Rule #3 (subset) — Typography fontSize={13} is the body default; drop it.
selector:
"JSXOpeningElement[name.name='Typography'] JSXAttribute[name.name='fontSize'] > JSXExpressionContainer > Literal[value=13]",
message:
"fontSize={13} is the Typography body default — drop the prop. For other sizes use typographyLevels tokens. See docs/frontend-conventions.md rule #3.",
},
],
};
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
...FRONTEND_CONVENTION_RULES,
},
globals: {
__APP_VERSION__: "readonly",
process: "readonly",
},
overrides: [
{
files: ["**/*.ts", "**/*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
plugins: ["@typescript-eslint", "react-refresh"],
rules: {
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
// TS handles these; eslint duplicates create noise on existing files.
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-non-null-assertion": "off",
...FRONTEND_CONVENTION_RULES,
},
},
],
};
+2 -2
View File
@@ -24,8 +24,8 @@ dist-ssr
*.sw?
.env
.env.*
# Scripts
*.sh
.env.development
!env.sh
-16
View File
@@ -1,16 +0,0 @@
{
"printWidth": 90,
"useTabs": true,
"tabWidth": 2,
"singleQuote": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"bracketSameLine": false,
"singleAttributePerLine": true,
"semi": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"arrowParens": "always",
"trailingComma": "es5",
"htmlWhitespaceSensitivity": "css"
}
+17
View File
@@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1.6
ARG NODE_VERSION=20
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Assets-only image: no web server. Contains built files at /dist.
FROM --platform=$TARGETPLATFORM busybox:stable
WORKDIR /
COPY --from=build /app/dist /dist
# No CMD provided intentionally — this image is for copying/volume population.
-18
View File
@@ -1,18 +0,0 @@
Checkmate
Copyright (c) BlueWave Labs
This product includes assets developed by third parties.
----------------------------------------------------------------------
King chess-piece silhouette used in the Checkmate logo
(client/src/assets/icons/checkmate-icon.svg and
client/public/checkmate_favicon.svg)
is adapted from "Chess klt45.svg" by Cburnett, available at:
https://commons.wikimedia.org/wiki/File:Chess_klt45.svg
Licensed under Creative Commons Attribution-ShareAlike 3.0 Unported
(CC BY-SA 3.0): https://creativecommons.org/licenses/by-sa/3.0/
The original artwork has been recolored from the white-with-black-stroke
chess piece to the Checkmate brand green (#13715B) for use as a logo.
Modifications: stroke color change only; geometry unchanged.
+73 -1
View File
@@ -1 +1,73 @@
This directory contains the client side (frontend) of Checkmate.
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+28
View File
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
]);
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('i18n-unused').RunOptions} */
module.exports = {
localesPath: "src/locales",
srcPath: "src",
translationKeyMatcher: /\bt\s*\(\s*['"`](.*?)['"`]/g,
};
+13 -20
View File
@@ -1,25 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="./checkmate_favicon.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Checkmate</title>
<script src="/config.js"></script>
</head>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./checkmate_favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkmate</title>
</head>
<body>
<div id="root"></div>
<script src="/env.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
+5661 -8072
View File
File diff suppressed because it is too large. Load diff
+56 -76
View File
@@ -1,78 +1,58 @@
{
"name": "client",
"private": true,
"version": "3.12.0",
"type": "module",
"scripts": {
"lockfile": "docker run --rm -v \"$PWD\":/w -w /w $(grep -m1 -o 'node:[^ ]*' ../docker/Dockerfile) npm install --package-lock-only",
"dev": "vite",
"build": "tsc -b && vite build",
"build-dev": "tsc -b && vite build --mode development",
"lint": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives",
"lint-strict": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"format": "prettier --write .",
"format-check": "prettier --check .",
"compile": "tsc -b"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource-variable/geist": "^5.3.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
"@mui/lab": "7.0.0",
"@mui/material": "7.3.7",
"@mui/x-charts": "7.29.1",
"@mui/x-date-pickers": "7.29.4",
"@reduxjs/toolkit": "2.7.0",
"@types/maplibre-gl": "^1.13.2",
"axios": "^1.15.2",
"dayjs": "1.11.13",
"html2canvas": "^1.4.1",
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.4",
"lucide-react": "^0.562.0",
"maplibre-gl": "^5.19.0",
"mui-color-input": "^7.0.0",
"pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.63.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
"react-map-gl": "^8.1.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "24.5.2",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"lint-staged": "^16.2.7",
"mongoose": "9.6.2",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "^6.4.2"
}
"name": "checkmate",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"i18n:display": "i18n-unused display-unused > test.log",
"dev": "vite --host 127.0.0.1 --mode development",
"build": "tsc -b && vite build --mode production",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@hookform/resolvers": "5.2.2",
"@mui/material": "7.3.4",
"@mui/x-date-pickers": "8.16.0",
"@reduxjs/toolkit": "2.9.0",
"axios": "1.12.2",
"dayjs": "1.11.18",
"flag-icons": "7.5.0",
"human-interval": "2.0.1",
"lucide-react": "0.553.0",
"ms": "2.1.3",
"pretty-ms": "9.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "16.0.1",
"react-redux": "9.2.0",
"react-router": "7.9.4",
"react-router-dom": "7.9.4",
"react-toastify": "^11.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
"swr": "2.3.6",
"vite-plugin-svgr": "4.5.0",
"zod": "4.1.12"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/ms": "2.1.0",
"@types/node": "^24.10.1",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"i18n-unused": "0.19.0",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
}
}
@@ -1,6 +0,0 @@
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
http,HTTP Monitor,https://www.google.com,60000,,,,
http,API Monitor,http://reqres.in/api/users/2,120000,,Janet,data.first_name,equal
ping,Ping monitor,127.0.0.1,60000,,,,
docker,Docker monitor,bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01,60000,,,,
port,Port monitor,127.0.0.1,60000,5000,,,
1 type name url interval port expectedValue jsonPath matchMethod
2 http HTTP Monitor https://www.google.com 60000
3 http API Monitor http://reqres.in/api/users/2 120000 Janet data.first_name equal
4 ping Ping monitor 127.0.0.1 60000
5 docker Docker monitor bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01 60000
6 port Port monitor 127.0.0.1 60000 5000
@@ -1 +0,0 @@
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
1 type name url interval port expectedValue jsonPath matchMethod
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" width="45" height="45">
<title>Checkmate</title>
<desc>King silhouette adapted from Cburnett's chess set (Wikimedia Commons, CC BY-SA 3.0).</desc>
<g fill="none" stroke="#13715B" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 819 B

-4
View File
@@ -1,4 +0,0 @@
// Fallback so /config.js always resolves (e.g. under the Vite dev server). In
// the Docker image the server shadows this path and renders the real config
// from its CLIENT_CONFIG_* environment variables.
window.__CHECKMATE_CONFIG__ = {};
+7
View File
@@ -0,0 +1,7 @@
window.__ENV = {
GLOBAL_REFRESH: "60000",
APP_HOST: "http://localhost:5173",
APP_API_BASE_URL: "http://localhost:52345/api/v1",
APP_DEPLOYMENT_MODE: "self_hosted",
// APP_DEPLOYMENT_MODE: "saas",
};
-5
View File
@@ -1,5 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"labels": ["dependencies"]
}
-258
View File
@@ -1,258 +0,0 @@
/**
* 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();
+30 -68
View File
@@ -1,75 +1,37 @@
import { type CSSProperties } from "react";
import { useSelector } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import CssBaseline from "@mui/material/CssBaseline";
import AppRoutes from "@/routes/Router";
import { ThemeProvider } from "@mui/material/styles";
import { lightTheme, darkTheme } from "@/theme/theme";
import { I18nLoader } from "@/components/i18nLoader";
import { useAppSelector } from "./hooks/AppHooks";
import { BrowserRouter } from "react-router-dom";
import { AuthVerifier } from "@/components/auth-verifier";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { SWRConfig } from "swr";
import { Routes } from "./Routes";
import AppLayout from "@/Components/layout/AppLayout";
import type { RootState } from "@/Types/state";
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
const swrConfig = {
dedupingInterval: 5000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
shouldRetryOnError: false,
errorRetryCount: 2,
errorRetryInterval: 1000,
focusThrottleInterval: 5000,
};
function App() {
const mode = useSelector((state: RootState) => state.ui.mode);
const theme = mode === "light" ? lightTheme : darkTheme;
const mode = useAppSelector((state) => state.ui.mode);
return (
<SWRConfig value={swrConfig}>
<ThemeProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
body: {
backgroundColor: theme.palette.background.default,
},
".Toastify__progress-bar": {
height: 2,
bottom: 0,
top: "auto",
},
".Toastify__progress-bar--bg": {
display: "none",
},
}}
/>
<AppLayout>
<Routes />
</AppLayout>
<ToastContainer
newestOnTop={true}
theme={mode}
icon={false}
style={
{
"--toastify-color-progress-light": theme.palette.primary.main,
"--toastify-color-progress-dark": theme.palette.primary.main,
"--toastify-toast-min-height": "40px",
"--toastify-toast-padding": "8px 12px",
"--toastify-toast-width": "auto",
} as CSSProperties
}
toastStyle={{
minHeight: 40,
padding: "8px 12px",
width: "auto",
maxWidth: "min(560px, calc(100vw - 32px))",
}}
progressStyle={{ height: 2 }}
/>
</ThemeProvider>
</SWRConfig>
);
return (
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<CssBaseline />
<I18nLoader />
<BrowserRouter>
<AuthVerifier>
<AppRoutes />
</AuthVerifier>
</BrowserRouter>
<ToastContainer
newestOnTop={true}
theme={mode}
style={
{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
} as React.CSSProperties
}
/>
</ThemeProvider>
);
}
export default App;
@@ -1,95 +0,0 @@
import { useTranslation } from "react-i18next";
import { Box, MenuItem, Select, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import "flag-icons/css/flag-icons.min.css";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "../Features/UI/uiSlice";
const langMap = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
};
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const { language = "en" } = useSelector((state) => state.ui);
const dispatch = useDispatch();
const handleChange = (event) => {
const newLang = event.target.value;
dispatch(setLanguage(newLang));
};
const languages = Object.keys(i18n.options.resources || {});
return (
<Select
value={language}
onChange={handleChange}
size="small"
sx={{
minWidth: 80,
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
"& .MuiSelect-icon": {
alignSelf: "center",
},
}}
>
{languages.map((lang) => {
let parsedLang = lang === "en" ? "gb" : lang;
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
}
parsedLang = langMap[parsedLang] || parsedLang;
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
return (
<MenuItem
key={lang}
value={lang}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
justifyContent="center"
>
<Box
component="span"
sx={{
display: "flex",
alignItems: "center",
}}
>
{flag && <span className={flag} />}
</Box>
<Box
component="span"
sx={{ textTransform: "uppercase" }}
>
{lang}
</Box>
</Stack>
</MenuItem>
);
})}
</Select>
);
};
export default LanguageSelector;
@@ -1,65 +0,0 @@
import React, { useState } from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import IconButton from "@mui/material/IconButton";
import { Settings } from "lucide-react";
import { useTheme } from "@mui/material/styles";
export type ActionMenuItem = {
id: number | string;
label: React.ReactNode;
action: Function;
closeMenu?: boolean;
};
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
const theme = useTheme();
const [anchorEl, setAnchorEl] = useState<null | any>(null);
const open = Boolean(anchorEl);
const handleClick = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
};
const handleClose = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(null);
};
return (
<div>
<IconButton
onClick={handleClick}
sx={{
"&:hover": {
backgroundColor: theme.palette.action.controlHover,
},
}}
>
<Settings
size={20}
strokeWidth={1.5}
/>
</IconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
{items.map((item) => (
<MenuItem
key={item.id}
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
e.stopPropagation();
if (item.closeMenu) handleClose(e);
item.action();
}}
>
{item.label}
</MenuItem>
))}
</Menu>
</div>
);
};
@@ -1,140 +0,0 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { CheckSnapshot } from "@/Types/Check";
import { getResponseColor } from "@/Utils/DataUtils";
import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip";
import type { SxProps } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
interface PlaceholderCheck {
status: "placeholder";
responseTime: 0;
createdAt: "";
}
interface HeatmapResponseTimeProps {
checks: CheckSnapshot[];
gap?: ResponsiveStyleValue<number | string>;
availabilityCellSx?: SxProps;
responseCellSx?: SxProps;
}
export const HeatmapResponseTime = ({
checks,
gap,
availabilityCellSx,
responseCellSx,
}: HeatmapResponseTimeProps) => {
const theme = useTheme();
if (!gap) {
gap = theme.spacing(0.5);
}
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-MAX_RECENT_CHECKS);
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== MAX_RECENT_CHECKS) {
const placeholders = Array(MAX_RECENT_CHECKS - latestChecks.length).fill({
status: "placeholder" as const,
});
data = [...latestChecks, ...placeholders];
} else {
data = latestChecks;
}
return (
<Box width={"100%"}>
<Box
width={"100%"}
display={"grid"}
gap={gap}
sx={{
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "stretch",
}}
>
{data.map((check, index) => {
const statusBg =
check.status === "placeholder"
? theme.palette.action.hover
: check.status === true
? theme.palette.success.light
: theme.palette.error.light;
const respBg =
check.status === "placeholder"
? theme.palette.action.hover
: getResponseColor(check.responseTime || 0, {
start: theme.palette.success.main,
mid: theme.palette.warning.main,
end: theme.palette.error.main,
});
const statusBorder =
check.status === "placeholder"
? theme.palette.divider
: check.status === true
? theme.palette.success.main
: theme.palette.error.main;
return (
<HeatmapResponseTimeTooltip
key={`${check}-${index}`}
check={check}
>
<Box
display={"grid"}
sx={{
gridTemplateRows: "auto auto",
}}
>
<Box
display={"flex"}
gap={theme.spacing(2)}
sx={{
flexDirection: "column",
}}
>
<Box
width={"100%"}
sx={{
position: "relative",
aspectRatio: "10",
}}
>
<Box
bgcolor={statusBg}
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
...availabilityCellSx,
}}
/>
<Box
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
pointerEvents: "none",
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
}}
/>
</Box>
<Box
width={"100%"}
bgcolor={respBg}
borderRadius={theme.shape.borderRadius}
sx={{
aspectRatio: "1 / 1",
...responseCellSx,
}}
/>
</Box>
</Box>
</HeatmapResponseTimeTooltip>
);
})}
</Box>
</Box>
);
};
@@ -1,77 +0,0 @@
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { formatDateWithTz, formatMs } from "@/Utils/TimeUtils";
import type { CheckSnapshot } from "@/Types/Check";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { useTranslation } from "react-i18next";
type HeatmapCheck =
| CheckSnapshot
| { status: "placeholder"; responseTime: 0; createdAt: "" };
export const HeatmapResponseTimeTooltip = ({
children,
check,
}: {
children: React.ReactElement;
check: HeatmapCheck;
}) => {
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const theme = useTheme();
const getColor = (status: boolean) => {
if (status === true) return theme.palette.success.light;
if (status === false) return theme.palette.error.light;
};
if (check.status === "placeholder") {
return children;
}
return (
<Tooltip
slotProps={{
tooltip: {
sx: {
backgroundColor: "transparent",
color: "inherit",
boxShadow: "none",
},
},
}}
title={
<Stack
sx={{
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
p: theme.spacing(4),
}}
>
<Typography>
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography>
<Typography>
{t("common.labels.responseTime")}:{" "}
{check?.responseTime ? formatMs(check.responseTime) : "N/A"}
</Typography>
<Typography textTransform={"capitalize"}>
Status:{" "}
<span style={{ color: getColor(check?.status) }}>
{check?.status === true
? t("pages.common.monitors.status.up")
: t("pages.common.monitors.status.down")}
</span>
</Typography>
</Stack>
}
>
{children}
</Tooltip>
);
};
@@ -1,166 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
import { computeBarHeights } from "@/Utils/DataUtils";
import { formatMs } from "@/Utils/TimeUtils";
interface HistogramResponseTimeProps {
checks: CheckSnapshot[];
height?: ResponsiveStyleValue<number | string>;
gap?: ResponsiveStyleValue<number | string>;
showStats?: boolean;
statsPosition?: "left" | "right";
}
interface ResponseTimeStats {
max: string | null;
avg: string | null;
}
const DEFAULT_HEIGHT = 50;
const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => {
if (!Array.isArray(checks) || checks.length === 0) {
return { max: null, avg: null };
}
const validChecks = checks.filter((check) => check.responseTime != null);
if (validChecks.length === 0) {
return { max: null, avg: null };
}
const responseTimes = validChecks.map((check) => check.responseTime);
const max = formatMs(Math.max(...responseTimes));
const avg = formatMs(
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
);
return { max, avg };
};
export const HistogramResponseTime = ({
checks,
height = DEFAULT_HEIGHT,
gap,
showStats = true,
statsPosition = "right",
}: HistogramResponseTimeProps) => {
const theme = useTheme();
const { t } = useTranslation();
const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]);
const barHeights = useMemo(() => computeBarHeights(checks), [checks]);
if (!Array.isArray(checks) || checks.length === 0) return null;
const data =
checks.length !== MAX_RECENT_CHECKS
? [
...checks,
...Array(MAX_RECENT_CHECKS - checks.length).fill({ status: "placeholder" }),
]
: checks;
const chartHeight = typeof height === "number" ? `${height}px` : height;
const gridGap = gap ?? theme.spacing(0.5);
const statsContent = showStats && (stats.max !== null || stats.avg !== null) && (
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
width={110}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
>
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
);
return (
<Stack
direction={statsPosition === "left" ? "row-reverse" : "row"}
alignItems="center"
width={"100%"}
>
<Box sx={{ flex: 1 }}>
<Box
width={"100%"}
display={"grid"}
gap={gridGap}
height={chartHeight}
sx={{
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "end",
}}
>
{data.map((check, index) => {
const isPlaceholder = (check as any).status === "placeholder";
const heightPct = `${barHeights[index] ?? 0}%`;
const barColor =
check.status === true
? theme.palette.success.light
: theme.palette.error.light;
const bar = (
<Box
width={"100%"}
height={"100%"}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
position: "relative",
overflow: "hidden",
}}
>
<Box
width={"100%"}
height={isPlaceholder ? 0 : heightPct}
bgcolor={barColor}
sx={{
position: "absolute",
bottom: 0,
left: 0,
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
);
return isPlaceholder ? (
<Box
height={"100%"}
key={index}
>
{bar}
</Box>
) : (
<HeatmapResponseTimeTooltip
key={index}
check={check}
>
{bar}
</HeatmapResponseTimeTooltip>
);
})}
</Box>
</Box>
{statsContent}
</Stack>
);
};
@@ -1,47 +0,0 @@
import { Stack } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { ButtonInput } from "../../inputs/Button";
import { useTranslation } from "react-i18next";
interface HeaderCreateProps {
label?: string;
path: string;
isLoading?: boolean;
isAdmin?: boolean;
}
export const HeaderCreate = ({
label,
path,
isLoading = false,
isAdmin = false,
}: HeaderCreateProps) => {
const navigate = useNavigate();
const { t } = useTranslation();
if (!isAdmin) {
return null;
}
const handleClick = () => {
navigate(path);
};
return (
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
width="100%"
>
<ButtonInput
variant="contained"
color="primary"
onClick={handleClick}
loading={isLoading}
>
{label || t("common.buttons.create")}
</ButtonInput>
</Stack>
);
};
@@ -1,83 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { ToggleButtonGroup, ToggleButton } from "@/Components/inputs";
import { useTheme } from "@mui/material/styles";
import CircularProgress from "@mui/material/CircularProgress";
import { useTranslation } from "react-i18next";
import { useMediaQuery } from "@mui/material";
import type { DateRange } from "@/Types/Query";
interface MonitorTimeFrameHeaderProps {
isLoading?: boolean;
hasDateRange?: boolean;
dateRange: DateRange;
setDateRange: (dateRange: DateRange) => void;
}
export const HeaderTimeRange = ({
isLoading = false,
hasDateRange = true,
dateRange,
setDateRange,
}: MonitorTimeFrameHeaderProps) => {
const theme = useTheme();
const { t } = useTranslation();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
const handleChange = (
_event: React.MouseEvent<HTMLElement>,
newValue: DateRange | null
) => {
if (newValue !== null) {
setDateRange(newValue);
}
};
let timeFramePicker = null;
if (hasDateRange) {
timeFramePicker = (
<ToggleButtonGroup
orientation={isSmallScreen ? "vertical" : "horizontal"}
value={dateRange}
exclusive
onChange={handleChange}
size="small"
fullWidth={isSmallScreen}
>
<ToggleButton value="recent">
{t("components.headerTimeRange.labels.recent")}
</ToggleButton>
<ToggleButton value="day">
{t("components.headerTimeRange.labels.day")}
</ToggleButton>
<ToggleButton value="week">
{t("components.headerTimeRange.labels.week")}
</ToggleButton>
<ToggleButton value="month">
{t("components.headerTimeRange.labels.month")}
</ToggleButton>
</ToggleButtonGroup>
);
}
return (
<Stack
direction={{ xs: "column", md: "row" }}
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
>
<Box
width={16}
height={16}
>
{isLoading && <CircularProgress size={16} />}
</Box>
<Typography variant="body2">
{t(`components.headerTimeRange.description.${dateRange}`)}
</Typography>
{timeFramePicker}
</Stack>
);
};
-5
View File
@@ -1,5 +0,0 @@
export * from "./charts/HistogramResponseTime";
export * from "./charts/HeatmapResponseTime";
export * from "./charts/HeatmapResponseTimeTooltip";
export * from "./controls/HeaderCreate";
export * from "./controls/HeaderTimeRange";
@@ -1,48 +0,0 @@
import { Avatar as MuiAvatar } from "@mui/material";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material";
import type { RootState } from "@/Types/state";
interface AvatarProps {
src?: string;
small?: boolean;
sx?: object;
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
}
export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => {
const { user } = useSelector((state: RootState) => state.auth);
const theme = useTheme();
if (!user) return null;
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const [image, setImage] = useState<string>();
useEffect(() => {
if (user.avatarImage) {
setImage(`data:image/png;base64,${user.avatarImage}`);
}
}, [user?.avatarImage]);
return (
<MuiAvatar
onClick={onClick}
alt={`${user?.firstName} ${user?.lastName}`}
src={src ? src : user?.avatarImage ? image : undefined}
sx={{
color: theme.palette.primary.contrastText,
fontSize: small ? "16px" : "22px",
fontWeight: 400,
backgroundColor: theme.palette.primary.main,
display: "inline-flex",
...style,
...sx,
}}
>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0) || ""}
</MuiAvatar>
);
};
@@ -1,23 +0,0 @@
import Box, { type BoxProps } from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { forwardRef } from "react";
export const BaseBox = forwardRef<HTMLDivElement, BoxProps>(({ sx, ...rest }, ref) => {
const theme = useTheme();
return (
<Box
ref={ref}
{...rest}
sx={{
backgroundColor: theme.palette.background.paper,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
...sx,
}}
/>
);
});
BaseBox.displayName = "BaseBox";
@@ -1,77 +0,0 @@
import { BaseBox } from ".";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { LAYOUT } from "@/Utils/Theme/constants";
import type { ResponsiveStyleValue } from "@mui/system";
import { useTheme } from "@mui/material/styles";
type BaseChartProps = React.PropsWithChildren<{
icon?: React.ReactNode;
title: string;
rightTitle?: string;
width?: number | string;
maxWidth?: number | string;
flexBasis?: number | string;
padding?: number | string | ResponsiveStyleValue<number | string>;
onClick?: () => void;
}>;
export const BaseChart = ({
children,
title,
rightTitle,
width = "100%",
maxWidth = "100%",
flexBasis = "0%",
padding,
onClick,
}: BaseChartProps) => {
const theme = useTheme();
return (
<BaseBox
onClick={onClick}
sx={{
padding: padding ?? theme.spacing(LAYOUT.MD),
display: "flex",
flex: 1,
flexBasis,
width: width,
maxWidth: maxWidth,
...(onClick && {
cursor: "pointer",
"& .recharts-wrapper": { cursor: "pointer !important" },
}),
}}
>
<Stack
gap={theme.spacing(LAYOUT.MD)}
flex={1}
>
<Stack
direction="row"
justifyContent={"space-between"}
>
<Typography
variant="eyebrow"
color="text.secondary"
sx={{ overflowWrap: "anywhere" }}
>
{title}
</Typography>
{rightTitle && (
<Typography
variant="eyebrow"
color="text.secondary"
sx={{ overflowWrap: "anywhere" }}
>
{rightTitle}
</Typography>
)}
</Stack>
<Box flex={1}>{children}</Box>
</Stack>
</BaseBox>
);
};
@@ -1,394 +0,0 @@
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import { LAYOUT } from "@/Utils/Theme/constants";
import {
ErrorFallback,
EmptyFallback,
EmptyMonitorFallback,
} from "@/Components/design-elements/Fallback";
import { EmptyState } from "@/Components/design-elements/EmptyState";
import { NoticeBanner } from "@/Components/design-elements/NoticeBanner";
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
import { PageHeader } from "@/Components/design-elements/PageHeader";
import CircularProgress from "@mui/material/CircularProgress";
import { LanguageSelector, SwitchTheme } from "@/Components/inputs";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { Trans, useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
import { Typography } from "@mui/material";
export const PageSpeedKeyPriorityFallback = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<EmptyState
fullscreen
title={t("pages.pageSpeed.fallback.title")}
description={t("pages.pageSpeed.fallback.description")}
alert={
<NoticeBanner severity="warning">
<Trans
i18nKey="common.alerts.pageSpeedApiKey.content"
components={{
settingsLink: (
<Link
component={RouterLink}
to="/settings"
color={theme.palette.primary.main}
fontWeight={600}
sx={{
textDecoration: "underline",
}}
/>
),
}}
/>
</NoticeBanner>
}
/>
);
};
interface BasePageProps extends StackProps {
loading?: boolean;
error?: boolean;
children: React.ReactNode;
breadcrumbOverride?: string[];
headerKey?: string;
backTo?: string;
backLabel?: string;
}
export const BasePage = ({
loading,
error,
children,
breadcrumbOverride,
headerKey,
backTo,
backLabel,
...props
}: BasePageProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (loading) {
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{ height: "100%" }}
>
<CircularProgress
color="primary"
size={28}
/>
</Stack>
);
}
if (error) {
return (
<ErrorFallback
title="Something went wrong..."
subtitle="Please try again later"
/>
);
}
return (
<Stack
spacing={theme.spacing(LAYOUT.LG)}
{...props}
>
{headerKey ? (
<PageHeader
title={t(`pages.${headerKey}.header.title`)}
description={t(`pages.${headerKey}.header.description`)}
backTo={backTo}
backLabel={backLabel}
/>
) : (
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
)}
{children}
</Stack>
);
};
interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
totalCount: number;
description?: string;
page: string;
actionButtonText: string;
actionLink: string;
children: React.ReactNode;
headerKey?: string;
}
export const BasePageWithStates = ({
loading,
error,
totalCount,
page,
description,
actionButtonText,
actionLink,
children,
headerKey,
...props
}: BasePageWithStatesProps) => {
const showLoading = loading && totalCount === 0;
if (!loading && totalCount === 0) {
return (
<EmptyFallback
description={description}
title={page}
actionButtonText={actionButtonText}
actionLink={actionLink}
/>
);
}
return (
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
</BasePage>
);
};
interface MonitorBasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
totalCount: number;
page: string;
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
headerKey?: string;
}
export const MonitorBasePageWithStates = ({
loading,
error,
totalCount,
page,
actionLink,
children,
priorityFallback,
headerKey,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
const showLoading = loading && totalCount === 0;
if (priorityFallback) {
return <>{priorityFallback}</>;
}
if (!loading && totalCount === 0) {
return (
<EmptyMonitorFallback
page={page}
title={t(`pages.${page}.fallback.title`)}
description={t(`pages.${page}.fallback.description`)}
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
);
}
return (
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
</BasePage>
);
};
interface BaseAuthPageProps extends React.PropsWithChildren, StackProps {
title: string;
subtitle: string;
}
export const BaseAuthPage = ({
title,
subtitle,
children,
component,
onSubmit,
}: BaseAuthPageProps) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction={{ xs: "column", md: "row" }}
minHeight="100vh"
width="100%"
position="relative"
sx={{ backgroundColor: theme.palette.background.default }}
>
<Stack
flex={1}
alignItems="center"
justifyContent="center"
position="relative"
px={theme.spacing(LAYOUT.MD)}
py={{ xs: theme.spacing(20), md: theme.spacing(12) }}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
sx={{
position: "absolute",
top: theme.spacing(LAYOUT.MD),
right: theme.spacing(LAYOUT.MD),
zIndex: 3,
}}
>
<LanguageSelector />
<SwitchTheme />
</Stack>
<Box
component={component as React.ElementType}
onSubmit={onSubmit}
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(LAYOUT.MD),
width: "100%",
maxWidth: 360,
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mb={theme.spacing(2)}
>
<Box width={28}>
<Logo width="100%" />
</Box>
<Typography sx={{ fontWeight: 500, letterSpacing: "-0.01em", fontSize: 16 }}>
{t("common.appName")}
</Typography>
</Stack>
<Stack gap={theme.spacing(2)}>
<Typography
sx={{
fontSize: 26,
fontWeight: 400,
lineHeight: 1.15,
letterSpacing: "-0.02em",
color: theme.palette.text.primary,
}}
>
{title}
</Typography>
<Typography
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
}}
>
{subtitle}
</Typography>
</Stack>
{children}
</Box>
</Stack>
<Stack
flex={1}
justifyContent="space-between"
display={{ xs: "none", md: "flex" }}
sx={{
background: "linear-gradient(135deg, #0c4d3d 0%, #155a48 60%, #0f5d4a 100%)",
color: "#fff",
p: theme.spacing(28),
position: "relative",
overflow: "hidden",
"&::after": {
content: '""',
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.04) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.04) 75%)
`,
backgroundSize: "80px 80px",
backgroundPosition: "0 0, 0 40px, 40px -40px, -40px 0px",
maskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
WebkitMaskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
pointerEvents: "none",
},
}}
>
<div />
<Stack sx={{ position: "relative", zIndex: 1, gap: theme.spacing(4) }}>
<Typography
sx={{
fontSize: 13,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.12em",
color: "rgba(255,255,255,0.75)",
}}
>
{t("pages.auth.brandPanel.eyebrow")}
</Typography>
<Typography
sx={{
fontSize: 44,
fontWeight: 400,
lineHeight: 1.1,
letterSpacing: "-0.02em",
maxWidth: 460,
}}
>
{t("pages.auth.brandPanel.tagline")}
</Typography>
<Typography
sx={{
fontSize: 17,
lineHeight: 1.5,
color: "rgba(255,255,255,0.75)",
maxWidth: 460,
}}
>
{t("pages.auth.brandPanel.description")}
</Typography>
</Stack>
<Typography
sx={{
position: "relative",
zIndex: 1,
color: "rgba(255,255,255,0.5)",
fontSize: 12,
}}
>
v{__APP_VERSION__}
</Typography>
</Stack>
</Stack>
);
};
@@ -1,145 +0,0 @@
import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
import Typography from "@mui/material/Typography";
import { Link, useLocation } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { ChevronRight } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ReactNode } from "react";
const isId = (segment: string): boolean => {
return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
};
const actionSegments = ["create", "configure"];
const BreadcrumbWrapper = ({ children }: { children: ReactNode }) => {
const theme = useTheme();
return (
<MuiBreadcrumbs
separator={
<ChevronRight
size={16}
strokeWidth={1.5}
/>
}
sx={{
fontSize: "14px",
marginBottom: theme.spacing(6),
"& .MuiBreadcrumbs-separator": {
color: theme.palette.text.secondary,
},
}}
>
{children}
</MuiBreadcrumbs>
);
};
export const Breadcrumb = ({
breadcrumbOverride,
}: {
breadcrumbOverride?: string[] | undefined;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const location = useLocation();
// If override is an empty array, hide entirely
if (breadcrumbOverride !== undefined && breadcrumbOverride.length === 0) {
return null;
}
// If override has items, render them directly
if (breadcrumbOverride !== undefined && breadcrumbOverride.length > 0) {
return (
<BreadcrumbWrapper>
{breadcrumbOverride.map((item, index) => {
const isLast = index === breadcrumbOverride.length - 1;
return (
<Typography
key={index}
sx={{
fontSize: "14px",
fontWeight: isLast ? 600 : 400,
color: isLast ? theme.palette.primary.main : theme.palette.text.secondary,
}}
>
{item}
</Typography>
);
})}
</BreadcrumbWrapper>
);
}
// Default behavior: use location pathname
const segments = location.pathname.split("/").filter((x) => x);
if (segments.length === 0) {
return null;
}
// Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create"
const basePage = segments[0] || t("common.breadcrumbs.home");
const secondSegment = segments[1];
const thirdSegment = segments[2];
const isActionPage = secondSegment && actionSegments.includes(secondSegment);
const isDetailsPage = secondSegment && isId(secondSegment);
// page/section/:id routes such as docker/host/:monitorId
const isSectionDetailsPage = Boolean(
secondSegment && !isId(secondSegment) && thirdSegment && isId(thirdSegment)
);
const hasSubPage = isActionPage || isDetailsPage || isSectionDetailsPage;
const getSubPageLabel = (): string => {
if (isActionPage || isSectionDetailsPage) {
return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
}
return t("common.breadcrumbs.details");
};
return (
<BreadcrumbWrapper>
{hasSubPage ? (
<Link
to={`/${basePage}`}
style={{ textDecoration: "none" }}
>
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
}}
>
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
</Typography>
</Link>
) : (
<Typography
sx={{
fontSize: "14px",
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
</Typography>
)}
{hasSubPage && (
<Typography
sx={{
fontSize: "14px",
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
{getSubPageLabel()}
</Typography>
)}
</BreadcrumbWrapper>
);
};
@@ -1,48 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Check } from "lucide-react";
import { useTheme } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
export const BulletPointCheck = ({
text,
variant = "info",
}: {
text: string;
noHighlightText?: string;
variant?: "success" | "error" | "info";
}) => {
const theme = useTheme();
const colors: Record<string, string | undefined> = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.text.secondary,
};
return (
<Stack
direction="row"
gap={theme.spacing(LAYOUT.SM)}
alignItems="center"
>
<Check
size={16}
strokeWidth={1.5}
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
style={{
flexShrink: 0,
}}
/>
<Typography
component="span"
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
fontWeight={450}
sx={{
opacity: 0.9,
}}
>
{text}
</Typography>
</Stack>
);
};
@@ -1,33 +0,0 @@
import type { CSSProperties } from "react";
export const DotSizes = ["xs", "sm", "md", "lg"] as const;
export type DotSize = (typeof DotSizes)[number];
const DOT_SIZE_PX: Record<DotSize, string> = {
xs: "2px",
sm: "4px",
md: "8px",
lg: "12px",
};
interface DotProps {
color?: string;
size?: DotSize;
style?: CSSProperties;
}
export const Dot = ({ color = "gray", size = "sm", style }: DotProps) => {
return (
<span
style={{
content: '""',
width: DOT_SIZE_PX[size],
height: DOT_SIZE_PX[size],
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
@@ -1,158 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { Link as RouterLink } from "react-router-dom";
import { Button } from "@/Components/inputs";
import { LAYOUT } from "@/Utils/Theme/constants";
import { EmptyStateIllustration } from "./EmptyStateIllustration";
import type { ReactNode } from "react";
interface EmptyStateProps {
title: string;
description?: ReactNode;
actionText?: string;
actionTo?: string;
onAction?: () => void;
secondaryText?: string;
secondaryHref?: string;
compact?: boolean;
fullscreen?: boolean;
alert?: ReactNode;
children?: ReactNode;
}
export const EmptyState = ({
title,
description,
actionText,
actionTo,
onAction,
secondaryText,
secondaryHref,
compact = false,
fullscreen = false,
alert,
children,
}: EmptyStateProps) => {
const theme = useTheme();
if (compact) {
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.MD)}
sx={{ py: theme.spacing(LAYOUT.MD), px: theme.spacing(LAYOUT.SM) }}
>
<EmptyStateIllustration
width={80}
height={60}
/>
<Stack sx={{ flex: 1 }}>
<Typography
sx={{
fontSize: 14,
fontWeight: 400,
color: theme.palette.text.secondary,
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: 1.55,
}}
>
{description}
</Typography>
)}
</Stack>
</Stack>
);
}
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{
...(fullscreen && { minHeight: "65vh" }),
py: theme.spacing(LAYOUT.XXL),
px: theme.spacing(LAYOUT.MD),
textAlign: "center",
}}
>
<EmptyStateIllustration />
<Typography
sx={{
mt: theme.spacing(LAYOUT.MD),
mb: theme.spacing(LAYOUT.XS),
fontSize: 14,
fontWeight: 400,
color: theme.palette.text.secondary,
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
maxWidth: 360,
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: 1.55,
mb: theme.spacing(LAYOUT.LG),
}}
>
{description}
</Typography>
)}
{alert && (
<Stack sx={{ width: "100%", maxWidth: 520, mb: theme.spacing(LAYOUT.MD) }}>
{alert}
</Stack>
)}
{(actionText || secondaryText) && (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.MD)}
>
{actionText && (actionTo || onAction) && (
<Button
variant="contained"
color="primary"
{...(actionTo
? { component: RouterLink, to: actionTo }
: { onClick: onAction })}
>
{actionText}
</Button>
)}
{secondaryText && secondaryHref && (
<Typography
component="a"
href={secondaryHref}
target="_blank"
rel="noopener"
sx={{
fontSize: 13,
fontWeight: 500,
color: theme.palette.primary.main,
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{secondaryText} →
</Typography>
)}
</Stack>
)}
{children}
</Stack>
);
};
@@ -1,197 +0,0 @@
import { useTheme } from "@mui/material/styles";
interface EmptyStateIllustrationProps {
width?: number | string;
height?: number | string;
}
const PALETTE = {
light: {
grid: "#CBD5E1",
backFill: "#F5F7FA",
backStroke: "#D5DBE3",
backInner: "#B6C2CF",
frontFill: "#EAF5F0",
frontStroke: "#7DBFAA",
frontInner: "#4DAF94",
},
dark: {
grid: "#3A4452",
backFill: "#475569",
backStroke: "#64748B",
backInner: "#CBD5E1",
frontFill: "#0F5D4A",
frontStroke: "#4DAF94",
frontInner: "#B5E2D2",
},
};
export const EmptyStateIllustration = ({
width = 160,
height = 120,
}: EmptyStateIllustrationProps) => {
const theme = useTheme();
const p = theme.palette.mode === "dark" ? PALETTE.dark : PALETTE.light;
return (
<svg
viewBox="0 0 160 120"
width={width}
height={height}
aria-hidden="true"
>
<g
stroke={p.grid}
strokeWidth="0.75"
>
<line
x1="16"
y1="0"
x2="16"
y2="120"
/>
<line
x1="56"
y1="0"
x2="56"
y2="120"
strokeDasharray="3 3"
/>
<line
x1="104"
y1="0"
x2="104"
y2="120"
strokeDasharray="3 3"
/>
<line
x1="144"
y1="0"
x2="144"
y2="120"
/>
<line
x1="0"
y1="12"
x2="160"
y2="12"
/>
<line
x1="0"
y1="44"
x2="160"
y2="44"
strokeDasharray="3 3"
/>
<line
x1="0"
y1="76"
x2="160"
y2="76"
strokeDasharray="3 3"
/>
<line
x1="0"
y1="108"
x2="160"
y2="108"
/>
</g>
<g>
<rect
x="38"
y="22"
width="92"
height="42"
rx="6"
fill={p.backFill}
stroke={p.backStroke}
strokeWidth="0.5"
/>
<rect
x="48"
y="32"
width="14"
height="22"
rx="2"
fill={p.backInner}
opacity="0.5"
/>
<rect
x="68"
y="34"
width="50"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.7"
/>
<rect
x="68"
y="42"
width="38"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.55"
/>
<rect
x="68"
y="50"
width="44"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.55"
/>
<rect
x="22"
y="50"
width="92"
height="42"
rx="6"
fill={p.frontFill}
stroke={p.frontStroke}
strokeWidth="0.5"
strokeOpacity="0.4"
/>
<rect
x="32"
y="60"
width="14"
height="22"
rx="2"
fill={p.frontInner}
opacity="0.35"
/>
<rect
x="52"
y="62"
width="50"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.55"
/>
<rect
x="52"
y="70"
width="38"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.4"
/>
<rect
x="52"
y="78"
width="44"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.4"
/>
</g>
</svg>
);
};
@@ -1,85 +0,0 @@
import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
import { EmptyState } from "./EmptyState";
import type { StackProps } from "@mui/material/Stack";
interface BaseFallbackProps extends StackProps {
children: React.ReactNode;
}
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
const theme = useTheme();
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{ width: "100%", py: theme.spacing(LAYOUT.LG) }}
{...props}
>
{children}
</Stack>
);
};
export const ErrorFallback = ({
title,
subtitle,
}: {
title: string;
subtitle: string;
}) => {
return (
<EmptyState
fullscreen
title={title}
description={subtitle}
/>
);
};
export const EmptyFallback = ({
title,
description,
actionButtonText,
actionLink,
}: {
title: string;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
return (
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
export const EmptyMonitorFallback = ({
title,
description,
actionButtonText,
actionLink,
}: {
page: string;
title: string;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
return (
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
@@ -1,168 +0,0 @@
import { BaseChart } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import Box, { type BoxProps } from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { forwardRef, useMemo, useState, useEffect } from "react";
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
interface GaugeProps extends Omit<BoxProps, "color"> {
isLoading?: boolean;
progress?: number;
radius?: number;
strokeWidth?: number;
precision?: number;
unit?: string;
}
export const Gauge = forwardRef<HTMLDivElement, GaugeProps>(function Gauge(
{
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
precision = 1,
unit = "%",
...rest
},
ref
) {
const theme = useTheme();
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
circumference: 2 * Math.PI * radius,
totalSize: radius * 2 + strokeWidth * 2,
strokeLength: (progressWithinRange / 100) * (2 * Math.PI * radius),
}),
[radius, strokeWidth, progressWithinRange]
);
const [offset, setOffset] = useState(circumference);
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
setOffset(circumference - strokeLength);
}, 100);
return () => clearTimeout(timer);
}, [circumference, strokeLength]);
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
if (isLoading) {
return null;
}
return (
<Box
ref={ref}
display={"inline-block"}
position={"relative"}
width={radius}
height={radius}
bgcolor={theme.palette.background.paper}
borderRadius={"50%"}
{...rest}
>
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
>
<circle
stroke={theme.palette.secondary.main}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
<circle
stroke={fillColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
style={{
transform: "rotate(-90deg)",
transformOrigin: "center",
transition: "stroke-dashoffset 1.5s ease-in-out",
}}
/>
</svg>
<Typography
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
});
export const DetailGauge = ({
title,
progress,
upperLabel,
upperValue,
lowerLabel,
lowerValue,
maxWidth = 225,
flexBasis = "0%",
}: {
title: string;
progress: number;
upperLabel?: string;
upperValue?: string | number;
lowerLabel?: string;
lowerValue?: string | number;
maxWidth?: number;
flexBasis?: number | string;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={null}
title={title}
maxWidth={maxWidth}
flexBasis={flexBasis}
>
<Stack
alignItems={"center"}
mb={theme.spacing(4)}
gap={theme.spacing(4)}
>
<Gauge progress={progress} />
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{upperLabel}</Typography>
<Typography>{upperValue}</Typography>
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{lowerLabel}</Typography>
<Typography>{lowerValue}</Typography>
</Stack>
</BaseChart>
);
};
@@ -1,21 +0,0 @@
import type { LucideIcon } from "lucide-react";
interface IconProps {
icon: LucideIcon;
size?: number;
strokeWidth?: number;
stroke?: string;
color?: string;
}
const Icon = ({ icon: Icon, size = 16, strokeWidth = 1.5, color }: IconProps) => {
return (
<Icon
size={size}
strokeWidth={strokeWidth}
color={color}
/>
);
};
export default Icon;
@@ -1,70 +0,0 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
import { formatDuration } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const { t, i18n } = useTranslation();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
if (!monitor) {
return null;
}
return (
<Stack>
<Typography
fontSize={typographyLevels.xxl}
fontWeight={500}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(LAYOUT.XS)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={theme.typography.fontFamilyMonospace}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
>
{formatUrl(monitor?.url)}
</Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && (
<>
<Dot />
<Typography>
{t("pages.common.monitors.statBoxes.checkingEvery", {
interval: formatDuration(monitor.interval, {
long: true,
locale: i18n.resolvedLanguage,
}),
})}
</Typography>
</>
)}
</Stack>
</Stack>
);
};
@@ -1,55 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme, alpha } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
export const Severities = ["info", "warning", "error"] as const;
export type Severity = (typeof Severities)[number];
const SEVERITY_GLYPH: Record<Severity, string> = {
info: "ⓘ",
warning: "⚠",
error: "✕",
};
interface NoticeBannerProps {
severity?: Severity;
children: React.ReactNode;
}
export const NoticeBanner = ({ severity = "info", children }: NoticeBannerProps) => {
const theme = useTheme();
const tone = theme.palette[severity].main;
return (
<Stack
direction="row"
alignItems="flex-start"
gap={theme.spacing(LAYOUT.SM)}
width={"100%"}
p={theme.spacing(LAYOUT.MD)}
borderRadius={theme.shape.borderRadius}
border={`1px solid ${alpha(tone, 0.45)}`}
bgcolor={alpha(tone, 0.08)}
textAlign={"left"}
>
<Box
component="span"
color={tone}
fontSize={typographyLevels.xl}
lineHeight={1}
mt={LAYOUT.XXS}
aria-hidden
>
{SEVERITY_GLYPH[severity]}
</Box>
<Typography
color={theme.palette.text.primary}
lineHeight={1.55}
>
{children}
</Typography>
</Stack>
);
};
@@ -1,63 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { WifiOff } from "lucide-react";
import { useState, useEffect } from "react";
interface OfflineBannerProps {
visible: boolean;
}
export const OfflineBanner = ({ visible }: OfflineBannerProps) => {
const theme = useTheme();
const { t } = useTranslation();
const [shouldRender, setShouldRender] = useState(visible);
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (visible) {
setShouldRender(true);
requestAnimationFrame(() => setIsAnimating(true));
} else {
setIsAnimating(false);
const timer = setTimeout(() => setShouldRender(false), 1000);
return () => clearTimeout(timer);
}
}, [visible]);
if (!shouldRender) return null;
return (
<Box
sx={{
position: "fixed",
top: isAnimating ? 0 : "-100%",
left: 0,
right: 0,
zIndex: theme.zIndex.snackbar,
backgroundColor: theme.palette.error.dark,
color: "#FFFFFF",
px: theme.spacing(8),
py: theme.spacing(4),
transition: "top 1s ease-in-out",
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="center"
gap={theme.spacing(4)}
>
<WifiOff size={20} />
<Typography
variant="body2"
fontWeight={500}
>
{t("components.offlineBanner.serverUnreachable")}
</Typography>
</Stack>
</Box>
);
};
@@ -1,165 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Tooltip from "@mui/material/Tooltip";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
import type { ReactNode } from "react";
const EXTERNAL_LINKS = {
support: "https://discord.com/invite/NAb6H3UTjK",
docs: "https://checkmate.so/docs",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
interface PageHeaderProps {
title: string;
description?: ReactNode;
backTo?: string;
backLabel?: string;
}
export const PageHeader = ({
title,
description,
backTo,
backLabel,
}: PageHeaderProps) => {
const theme = useTheme();
const { t } = useTranslation();
const linkItems: { key: keyof typeof EXTERNAL_LINKS; icon: ReactNode }[] = [
{
key: "support",
icon: (
<HelpCircle
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "docs",
icon: (
<FileText
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "changelog",
icon: (
<Code
size={16}
strokeWidth={1.6}
/>
),
},
];
return (
<Stack
direction="row"
alignItems="flex-start"
justifyContent="space-between"
gap={6}
sx={{ mb: theme.spacing(8) }}
>
<Stack gap={theme.spacing(1)}>
{backTo && backLabel && (
<Box
component={RouterLink}
to={backTo}
sx={{
display: "inline-flex",
alignItems: "center",
gap: 0.5,
textDecoration: "none",
color: theme.palette.text.secondary,
fontSize: 13,
mb: theme.spacing(1),
"&:hover": { color: theme.palette.text.primary },
}}
>
<ChevronLeft
size={14}
strokeWidth={1.8}
/>
{backLabel}
</Box>
)}
<Typography
sx={{
fontSize: 26,
fontWeight: 400,
color: theme.palette.text.primary,
lineHeight: 1.15,
letterSpacing: "-0.02em",
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: 1.55,
maxWidth: "108ch",
}}
>
{description}
</Typography>
)}
</Stack>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: "hidden",
backgroundColor: theme.palette.background.paper,
flexShrink: 0,
}}
>
{linkItems.map((item, idx) => (
<Tooltip
key={item.key}
title={t(`components.pageHeader.links.${item.key}`)}
>
<Box
component="a"
href={EXTERNAL_LINKS[item.key]}
target="_blank"
rel="noopener noreferrer"
sx={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: INPUT_BASE_HEIGHT,
color: theme.palette.text.secondary,
borderRight:
idx < linkItems.length - 1
? `1px solid ${theme.palette.divider}`
: "none",
"&:hover": {
backgroundColor: theme.palette.action.hover,
color: theme.palette.text.primary,
},
}}
>
{item.icon}
</Box>
</Tooltip>
))}
</Box>
</Stack>
);
};
@@ -1,148 +0,0 @@
import { Box, Stack, useTheme } from "@mui/material";
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
interface SkeletonCardProps {
width?: number | string | { xs?: number | string; md?: number | string };
showHalo?: boolean;
}
export const SkeletonCard = ({
width = { xs: 130, md: 216 },
showHalo = true,
}: SkeletonCardProps) => {
const theme = useTheme();
const pulseAnimation = {
"@keyframes pulse": {
"0%, 100%": { opacity: 1 },
"50%": { opacity: 0.72 },
},
};
const floatAnimation = {
"@keyframes float": {
"0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" },
"50%": { transform: "rotate(5deg) translate(12px, -24px)" },
},
};
const cardStyle = {
width: width,
height: 57,
borderRadius: theme.shape.borderRadius,
background:
theme.palette.mode === "dark"
? theme.palette.secondary.dark
: theme.palette.background.paper,
boxShadow:
theme.palette.mode === "dark"
? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset"
: "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset",
display: "flex",
gap: theme.spacing(5),
alignItems: "center",
padding: theme.spacing(5),
};
const blockStyle = {
width: 55,
height: INPUT_BASE_HEIGHT,
borderRadius: theme.shape.borderRadius,
background: theme.palette.secondary.main,
border: `1px solid ${theme.palette.divider}`,
animation: "pulse 1.6s ease-in-out infinite",
...pulseAnimation,
};
const lineStyle = {
height: 7,
borderRadius: theme.shape.borderRadius,
background: theme.palette.secondary.main,
border: `1px solid ${theme.palette.divider}`,
animation: "pulse 1.6s ease-in-out infinite",
...pulseAnimation,
};
return (
<Box
position={"relative"}
mt={20}
mb={20}
>
{showHalo && (
<Box
sx={{
position: "absolute",
inset: 0,
display: "grid",
placeItems: "center",
pointerEvents: "none",
"&::before": {
content: '""',
width: "260px",
height: "260px",
borderRadius: "999px",
background:
theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "#fff",
filter: "blur(60px)",
opacity: 0.9,
},
}}
aria-hidden="true"
/>
)}
{/* Card stack */}
<Box sx={{ position: "relative", zIndex: 1 }}>
{/* Front card (now at bottom) */}
<Box sx={cardStyle}>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
{/* Empty space to maintain card height */}
</Stack>
</Box>
{/* Back card A */}
<Box
sx={{
...cardStyle,
transform: "rotate(-4deg) translate(-12px, -12px)",
filter: "blur(.2px)",
position: "absolute",
left: 0,
top: 0,
}}
aria-hidden="true"
>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
<Box sx={{ ...lineStyle, width: "60%" }} />
<Box sx={{ ...lineStyle, width: "80%" }} />
<Box sx={{ ...lineStyle, width: "40%" }} />
</Stack>
</Box>
{/* Back card B */}
<Box
sx={{
...cardStyle,
transform: "rotate(5deg) translate(12px, -20px)",
filter: "blur(.3px)",
position: "absolute",
left: 0,
top: 0,
animation: "float 3s ease-in-out 5",
...floatAnimation,
}}
aria-hidden="true"
>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
<Box sx={{ ...lineStyle, width: "60%" }} />
<Box sx={{ ...lineStyle, width: "80%" }} />
<Box sx={{ ...lineStyle, width: "40%" }} />
</Stack>
</Box>
</Box>
</Box>
);
};
@@ -1,79 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const SplitBox = ({
left,
right,
}: {
left: React.ReactNode;
right: React.ReactNode;
}) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Stack
direction={isSmall ? "column" : "row"}
bgcolor={theme.palette.background.paper}
border={1}
borderColor={theme.palette.divider}
borderRadius={theme.shape.borderRadius}
>
<Box
padding={theme.spacing(LAYOUT.XXL)}
borderRight={isSmall ? 0 : 1}
borderBottom={isSmall ? 1 : 0}
borderColor={theme.palette.divider}
flex={0.7}
sx={{
background:
theme.palette.mode === "dark"
? "linear-gradient(135deg, rgba(255, 255, 255, 0.01) 0%, rgba(255, 255, 255, 0.02) 100%)"
: "linear-gradient(135deg, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.02) 100%)",
}}
>
{left}
</Box>
<Box
flex={1}
padding={theme.spacing(LAYOUT.XXL)}
>
{right}
</Box>
</Stack>
);
};
export const ConfigBox = ({
title,
subtitle,
leftContent,
rightContent,
}: {
title: string;
subtitle: React.ReactNode;
leftContent?: React.ReactNode;
rightContent: React.ReactNode;
}) => {
const theme = useTheme();
return (
<SplitBox
left={
<Stack spacing={theme.spacing(LAYOUT.XS)}>
<Typography
component="h2"
variant="eyebrow"
color="text.secondary"
>
{title}
</Typography>
<Typography component="p">{subtitle}</Typography>
{leftContent}
</Stack>
}
right={rightContent}
/>
);
};
@@ -1,96 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { darken, lighten } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { PaletteKey } from "@/Utils/Theme/Theme";
import { BaseBox, TooltipWithInfo } from "@/Components/design-elements";
import type { SxProps } from "@mui/material";
type GradientBox = React.PropsWithChildren<{
palette?: PaletteKey;
sx?: SxProps;
interactive?: boolean;
}>;
export const GradientBox = ({ children, palette, sx, interactive }: GradientBox) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isLight = theme.palette.mode === "light";
const paper = theme.palette.background.paper;
const paperStart = isLight ? darken(paper, 0.02) : lighten(paper, 0.08);
const paperEnd = paper;
const bg = palette
? `linear-gradient(135deg, ${lighten(theme.palette[palette].main, 0.12)} 0%, ${theme.palette[palette].main} 100%)`
: `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
return (
<BaseBox
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
background: bg,
...(interactive && {
cursor: "pointer",
"&:hover": {
backgroundImage: `linear-gradient(${theme.palette.action.rowHover}, ${theme.palette.action.rowHover}), ${bg}`,
},
}),
...sx,
}}
>
{children}
</BaseBox>
);
};
type StatBoxProps = React.PropsWithChildren<{
title: string;
subtitle: string;
palette?: PaletteKey;
sx?: SxProps;
tooltip?: string;
onClick?: () => void;
}>;
export const StatBox = ({
title,
subtitle,
palette,
children,
sx,
tooltip,
onClick,
}: StatBoxProps) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox
palette={palette}
interactive={Boolean(onClick)}
sx={{ ...(sx as object) }}
>
<Stack onClick={onClick}>
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
<Typography
variant="eyebrow"
color={palette ? textColor : "text.secondary"}
>
{title}
</Typography>
{tooltip && (
<TooltipWithInfo
title={tooltip}
iconColor={textColor as string}
iconSize={14}
/>
)}
</Box>
<Typography color={textColor}>{subtitle}</Typography>
{children}
</Stack>
</GradientBox>
);
};
@@ -1,190 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/design-elements";
import { useTranslation } from "react-i18next";
import type { SxProps } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
type StatusBoxProps = React.PropsWithChildren<{
children: React.ReactNode;
sx?: SxProps;
}>;
export const BGBox = ({ children, sx }: StatusBoxProps) => {
const theme = useTheme();
return (
<BaseBox
sx={{
backgroundColor: theme.palette.background.default,
overflow: "hidden",
position: "relative",
flex: 1,
padding: theme.spacing(6),
...sx,
}}
>
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
sx={{
pointerEvents: "none",
backgroundImage: `
linear-gradient(${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px),
linear-gradient(90deg, ${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px)
`,
backgroundSize: "24px 24px",
maskImage:
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
WebkitMaskImage:
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
}}
/>
{children}
</BaseBox>
);
};
const StatusBox = ({
label,
n,
color,
sx,
}: {
label: string;
n: number;
color: string | undefined;
sx?: SxProps;
}) => {
const theme = useTheme();
return (
<BGBox sx={sx}>
<Stack spacing={theme.spacing(4)}>
<Typography
textTransform="uppercase"
color={theme.palette.text.secondary}
sx={{
fontSize: 11,
fontWeight: 500,
letterSpacing: "0.08em",
}}
>
{label}
</Typography>
<Typography
variant="h1"
color={color}
>
{n}
</Typography>
</Stack>
</BGBox>
);
};
export const UpStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.up")}
n={n}
color={theme.palette.success.light}
/>
);
};
export const DownStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.down")}
n={n}
color={theme.palette.error.light}
/>
);
};
export const PausedStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.paused")}
n={n}
color={theme.palette.warning.light}
/>
);
};
export const MaintenanceStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.maintenance")}
n={n}
color={theme.palette.warning.light}
/>
);
};
export const InitializingStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.initializing")}
n={n}
color={theme.palette.warning.light}
/>
);
};
export const BreachedStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.breached")}
n={n}
color={theme.palette.warning.main}
/>
);
};
export const TotalChecksBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.total")}
n={n}
color={theme.palette.primary.light}
/>
);
};
export const DownChecksBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.down")}
n={n}
color={theme.palette.error.light}
/>
);
};
export const UpChecksBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.up")}
n={n}
color={theme.palette.success.light}
/>
);
};
@@ -1,40 +0,0 @@
import { useTranslation } from "react-i18next";
import {
formatStatusCode,
getStatusCodeTooltip,
getStatusCodeValueType,
} from "@/Utils/statusCode";
import { ValueLabel } from "./StatusLabel";
import { Tooltip } from "./Tooltip";
export type StatusCodeLabelProps = {
statusCode: number | null | undefined;
message?: string | null;
fallback?: string;
};
export const StatusCodeLabel = ({
statusCode,
message,
fallback = "N/A",
}: StatusCodeLabelProps) => {
const { t } = useTranslation();
if (!statusCode) {
return <>{fallback}</>;
}
const text = formatStatusCode(statusCode, t);
const tooltip = getStatusCodeTooltip(statusCode, message, t);
const body = (
<ValueLabel
value={getStatusCodeValueType(statusCode)}
text={text}
/>
);
if (!tooltip) return body;
return (
<Tooltip title={tooltip}>
<span>{body}</span>
</Tooltip>
);
};
@@ -1,152 +0,0 @@
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/design-elements";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import type { MonitorStatus } from "@/Types/Monitor";
import type { SxProps } from "@mui/material/styles";
import {
getDockerStatePalette,
getStatusPalette,
getValuePalette,
} from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { DockerContainerState } from "@/Types/Check";
import { SPACING } from "@/Utils/Theme/constants";
export const ValueTypes = ["positive", "negative", "neutral"] as const;
export type ValueType = (typeof ValueTypes)[number];
export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => {
const { t } = useTranslation();
const theme = useTheme();
const palette = getStatusPalette(status);
const determineStatus = (status: MonitorStatus): string => {
if (status === "up") {
return t("pages.common.monitors.status.up");
} else if (status === "down") {
return t("pages.common.monitors.status.down");
} else if (status === "breached") {
return t("pages.common.monitors.status.breached");
} else if (status === "maintenance") {
return t("pages.common.monitors.status.maintenance");
} else if (status === "paused") {
return t("pages.common.monitors.status.paused");
} else if (status === "initializing") {
return t("pages.common.monitors.status.initializing");
}
return t("pages.common.monitors.status.initializing");
};
return (
<BaseBox
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
padding: theme.spacing(3, 5),
color: theme.palette[palette].main,
borderColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.08)",
...sx,
}}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[palette].light}
borderRadius="50%"
marginRight="5px"
/>
<Typography textTransform={"capitalize"}>{determineStatus(status)}</Typography>
</BaseBox>
);
};
export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) => {
const theme = useTheme();
const palette = getValuePalette(value);
const transformedText = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
return (
<BaseBox
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
padding: theme.spacing(3, 5),
color: theme.palette[palette].main,
borderColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.08)",
}}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[palette].light}
borderRadius="50%"
marginRight="5px"
/>
<Typography>{transformedText}</Typography>
</BaseBox>
);
};
export const ColoredLabel = ({ text, color }: { text: string; color: string }) => {
const theme = useTheme();
return (
<BaseBox
sx={{
width: "fit-content",
padding: theme.spacing(1, 3),
color: color,
borderColor: color,
}}
>
<Typography>{text}</Typography>
</BaseBox>
);
};
export const DockerStateLabel = ({
state,
name,
image,
}: {
state: DockerContainerState;
name: string;
image: string;
}) => {
const palette = getDockerStatePalette(state);
const theme = useTheme();
return (
<Stack
direction="row"
gap={SPACING.LG}
alignItems={"center"}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[palette].light}
borderRadius="50%"
marginRight="5px"
/>
<Stack>
<Typography>{name}</Typography>
<Typography variant="body2">{image}</Typography>
</Stack>
</Stack>
);
};
@@ -1,42 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { useTheme, alpha } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
interface StepProgressProps {
steps: number;
current: number;
}
export const StepProgress = ({ steps, current }: StepProgressProps) => {
const theme = useTheme();
const activeBg = `linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.primary.main} 100%)`;
const completedBg = `linear-gradient(135deg, ${alpha(theme.palette.primary.light, 0.5)} 0%, ${alpha(theme.palette.primary.main, 0.5)} 100%)`;
const getSegmentBackground = (index: number) => {
if (index < current) return completedBg;
if (index === current) return activeBg;
return theme.palette.divider;
};
return (
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
width="100%"
>
{Array.from({ length: steps }).map((_, index) => (
<Box
key={index}
flex={1}
height={theme.spacing(LAYOUT.XXS)}
borderRadius={theme.shape.borderRadius}
sx={{
background: getSegmentBackground(index),
transition: "background 0.2s ease",
}}
/>
))}
</Stack>
);
};
@@ -1,32 +0,0 @@
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { PageSpeedStrategy } from "@/Types/Monitor";
import { SPACING } from "@/Utils/Theme/constants";
interface StrategyBadgeProps {
strategy: PageSpeedStrategy | undefined;
variant?: "caption" | "body2";
}
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (!strategy) {
return null;
}
return (
<Typography
variant={variant}
px={SPACING.XXL}
py={SPACING.SM}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.secondary.light}
color={theme.palette.text.primary}
>
{t(`pages.pageSpeed.strategy.${strategy}`)}
</Typography>
);
};
@@ -1,644 +0,0 @@
import Stack from "@mui/material/Stack";
import Grid2 from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Collapse from "@mui/material/Collapse";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import {
ChevronsLeft,
ChevronsRight,
ChevronLeft,
ChevronRight,
Ellipsis,
} from "lucide-react";
import { EmptyState } from "./EmptyState";
import TablePagination from "@mui/material/TablePagination";
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
import { useTranslation } from "react-i18next";
import { useState, Fragment, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import type { SxProps, Theme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
export type Header<T> = {
id: number | string;
content: React.ReactNode;
mobileLabel?: React.ReactNode;
hideMobileLabel?: boolean;
align?: "left" | "center" | "right" | "justify" | "inherit";
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
headers: Header<T>[];
data: T[];
onRowClick?: (row: T) => void;
cardsOnSmallScreens?: boolean;
expandableRows?: boolean;
renderExpandedContent?: (row: T) => React.ReactNode;
emptyViewText?: string;
emptyViewPositive?: boolean;
getRowSx?: (row: T) => SxProps<Theme>;
isRowSelected?: (row: T) => boolean;
};
export function DataTable<
T extends {
id?: string | number;
_id?: string | number;
onRowClick?: (row: T) => void;
},
>({
headers,
data,
onRowClick,
cardsOnSmallScreens = true,
expandableRows = false,
renderExpandedContent,
emptyViewText,
emptyViewPositive,
getRowSx,
isRowSelected,
}: DataTableProps<T>) {
const theme = useTheme();
const [expanded, setExpanded] = useState<(string | number) | null>(null);
const handleExpand = (row: T) => {
const key = row.id || row._id || null;
setExpanded(expanded === key ? null : key);
};
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const hasTextSelectionWithin = (element: Element) => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
return false;
}
return (
selection.anchorNode !== null &&
element.contains(
selection.anchorNode.nodeType === Node.TEXT_NODE
? selection.anchorNode.parentNode
: selection.anchorNode
)
);
};
const isInteractive = Boolean(onRowClick) || expandableRows;
if (data.length === 0 || headers.length === 0) {
return (
<EmptyView
text={emptyViewText}
positive={emptyViewPositive}
/>
);
}
const keys = [];
// Return stack of cards for small screens
if (isSmall && cardsOnSmallScreens) {
return (
<Stack spacing={theme.spacing(LAYOUT.XS)}>
{data.map((row) => {
const key = row.id || row._id || Math.random();
keys.push(key);
return (
<Stack
onClick={(e) => {
if (hasTextSelectionWithin(e.currentTarget)) return;
if (onRowClick) onRowClick(row);
}}
spacing={theme.spacing(LAYOUT.XS)}
sx={{
borderStyle: "solid",
borderWidth: 1,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(LAYOUT.XS),
cursor: onRowClick ? "pointer" : "default",
...(onRowClick && {
"&:hover": {
backgroundColor: theme.palette.action.rowHover,
},
}),
}}
key={key}
>
{headers.map((header) => {
if (header.hideMobileLabel) {
return (
<Grid2
container
key={header.id}
>
<Grid2
size={12}
display="flex"
alignItems="center"
justifyContent="flex-end"
>
{header.render(row)}
</Grid2>
</Grid2>
);
}
return (
<Grid2
container
key={header.id}
>
<Grid2
size={5}
display={"flex"}
alignItems={"center"}
>
<Typography
component="div"
color={theme.palette.text.primary}
>
{header.mobileLabel ?? header.content}
</Typography>
</Grid2>
<Grid2
size={7}
display="flex"
alignItems={"center"}
>
{header.render(row)}{" "}
</Grid2>
</Grid2>
);
})}
{expandableRows && renderExpandedContent && renderExpandedContent(row) && (
<Grid2 size={12}>{renderExpandedContent(row)}</Grid2>
)}
</Stack>
);
})}
</Stack>
);
}
return (
<TableContainer
component={Paper}
elevation={0}
sx={{ boxShadow: "none", overflowX: "hidden" }}
>
<Table
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(LAYOUT.MD),
},
"& .MuiTableCell-root": {
borderBottom: `1px solid ${theme.palette.divider}`,
},
"& .MuiTableHead-root .MuiTableRow-root": {
height: "28px",
borderBottom: `1px solid ${theme.palette.divider}`,
},
"& .MuiTableHead-root .MuiTableCell-root": {
borderBottom: "none",
},
"& :is(th)": {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
fontSize: 11,
},
"& :is(td)": {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
padding: `${theme.spacing(LAYOUT.MD)} ${theme.spacing(LAYOUT.MD)}`,
fontSize: theme.typography.fontSize,
overflowX: "hidden",
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
<TableRow>
{headers.map((header, idx) => {
return (
<TableCell
align={header.align ?? (idx === 0 ? "left" : "center")}
key={header.id}
>
{header.content}
</TableCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
{data.map((row) => {
const key = row.id || row._id || Math.random();
const isExpanded = expanded === key;
const selected = isRowSelected?.(row);
return (
<Fragment key={key}>
<TableRow
sx={{
cursor: isInteractive ? "pointer" : "default",
...(isInteractive && {
"&:hover .MuiTableCell-root": {
backgroundColor: selected
? theme.palette.action.selectedHover
: theme.palette.action.rowHover,
},
}),
...(selected && {
"& .MuiTableCell-root": {
backgroundColor: theme.palette.action.selected,
},
}),
...(getRowSx?.(row) as object),
}}
onClick={(e) => {
if (hasTextSelectionWithin(e.currentTarget)) return;
if (expandableRows) handleExpand(row);
else if (onRowClick) onRowClick(row);
}}
>
{headers.map((header, index) => {
return (
<TableCell
align={header.align ?? (index === 0 ? "left" : "center")}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
{expandableRows &&
renderExpandedContent &&
renderExpandedContent(row) && (
<TableRow>
<TableCell
colSpan={headers.length}
style={{
borderBottom: isExpanded ? undefined : "none",
paddingTop: 0,
paddingBottom: 0,
}}
>
<Collapse
in={isExpanded}
timeout="auto"
unmountOnExit
>
<Box sx={{ pt: 4, pb: 4 }}>{renderExpandedContent(row)}</Box>
</Collapse>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
interface TablePaginationActionsProps {
count: number;
page: number;
rowsPerPage: number;
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
}
const controlHoverSx = (theme: Theme): SxProps<Theme> => ({
"&:hover": {
backgroundColor: theme.palette.action.controlHover,
},
});
function TablePaginationActions(props: TablePaginationActionsProps) {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
className="table-pagination-actions"
>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronsRight
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronsLeft
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronRight
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronLeft
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronLeft
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronRight
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronsLeft
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronsRight
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
</Box>
);
}
interface HasMoreTablePaginationActionsProps {
hasMore?: boolean;
page: number;
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
}
function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) {
const theme = useTheme();
const { hasMore, page, onPageChange } = props;
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page + 1);
};
return (
<Box
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
className="table-pagination-actions"
>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronsRight
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronsLeft
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronRight
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronLeft
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={hasMore === false}
aria-label="next page"
sx={controlHoverSx(theme)}
>
{theme.direction === "rtl" ? (
<ChevronLeft
size={20}
strokeWidth={1.5}
/>
) : (
<ChevronRight
size={20}
strokeWidth={1.5}
/>
)}
</IconButton>
<Tooltip title={hasMore === true ? "More pages available" : "No more pages"}>
<IconButton
disabled={hasMore === false}
aria-label="next page"
sx={controlHoverSx(theme)}
>
<Ellipsis
size={20}
strokeWidth={1.5}
/>
</IconButton>
</Tooltip>
</Box>
);
}
interface PaginationProps extends TablePaginationOwnProps {
component?: React.ElementType;
hasMore?: boolean;
itemsOnPage?: number;
}
export const Pagination = ({ ...props }: PaginationProps) => {
const { hasMore, itemsOnPage, ...rest } = props;
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
const theme = useTheme();
useEffect(() => {
if (
typeof itemsOnPage === "number" &&
itemsOnPage === 0 &&
rest.count > 0 &&
rest.page > 0 &&
rest.onPageChange
) {
rest.onPageChange(null, rest.page - 1);
}
}, [itemsOnPage, rest.count, rest.page, rest.onPageChange]);
const labelDisplayedRows = ({
from,
to,
count,
}: {
from: number;
to: number;
count: number;
}) => {
if (hasMore) {
return to === 0 ? "" : `${from}–${to}`;
}
return `${from}–${to} of ${count}`;
};
return (
<TablePagination
ActionsComponent={(props) =>
typeof hasMore === "boolean" ? (
<HasMoreTablePaginationActions
{...props}
hasMore={hasMore}
/>
) : (
<TablePaginationActions {...props} />
)
}
rowsPerPageOptions={[5, 10, 25]}
labelDisplayedRows={labelDisplayedRows}
{...rest}
sx={{
"& .MuiTablePagination-toolbar": isSmall
? {
display: "grid",
gridTemplateColumns: "1fr 1fr",
gridAutoRows: "auto",
rowGap: theme.spacing(SPACING.SM),
alignItems: "center",
justifyItems: "center",
paddingLeft: 0,
paddingRight: 0,
}
: { display: "flex", alignItems: "center" },
"& .MuiTablePagination-selectLabel": {
gridColumn: "1",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-select": {
gridColumn: "2",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-displayedRows": isSmall
? { gridColumn: "1 / span 2", gridRow: "2", justifySelf: "center" }
: {},
"& .table-pagination-actions": isSmall
? { gridColumn: "1 / span 2", gridRow: "3", justifySelf: "center" }
: {},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
},
}}
/>
);
};
const EmptyView = ({ text }: { text?: string; positive?: boolean }) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<Box
sx={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper,
}}
>
<EmptyState title={text ?? t("common.table.empty")} />
</Box>
);
};
@@ -1,65 +0,0 @@
import MuiTabs from "@mui/material/Tabs";
import type { TabsProps } from "@mui/material/Tabs";
import { useTheme } from "@mui/material/styles";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
interface CustomTabsProps extends TabsProps {}
export const Tabs = (props: CustomTabsProps) => {
const theme = useTheme();
return (
<MuiTabs
sx={{
minHeight: INPUT_BASE_HEIGHT,
borderBottom: `1px solid ${theme.palette.divider}`,
"& .MuiTabs-indicator": {
backgroundColor: theme.palette.primary.main,
height: 2,
bottom: 0,
},
"& .MuiTabs-flexContainer": {
gap: theme.spacing(16),
},
}}
{...props}
>
{props.children}
</MuiTabs>
);
};
import MuiTab from "@mui/material/Tab";
import type { TabProps } from "@mui/material/Tab";
interface CustomTabProps extends TabProps {}
export const Tab = (props: CustomTabProps) => {
const theme = useTheme();
return (
<MuiTab
disableRipple
iconPosition="start"
sx={{
textTransform: "none",
fontSize: typographyLevels.m,
fontWeight: 500,
minHeight: INPUT_BASE_HEIGHT,
padding: theme.spacing(1, 0),
paddingBottom: 0,
minWidth: "auto",
alignItems: "flex-start",
color: theme.palette.text.secondary,
"&.Mui-selected": {
color: theme.palette.primary.main,
},
"&:hover": {
color: theme.palette.text.secondary,
},
"& .MuiTab-iconWrapper": {
marginRight: theme.spacing(2),
marginBottom: 0,
},
}}
{...props}
/>
);
};
@@ -1,43 +0,0 @@
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import type { StackProps } from "@mui/material/Stack";
import Link from "@mui/material/Link";
import { Link as RouterLink } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
interface TextLinkProps extends StackProps {
text: string;
linkText: string;
href: string;
target?: "_self" | "_blank" | "_parent" | "_top";
}
export const TextLink = ({
text,
linkText,
href,
target = "_self",
...props
}: TextLinkProps) => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
{...props}
>
<Typography>{text}</Typography>
<Link
color="primary"
to={href}
component={RouterLink}
target={target}
{...(target === "_blank" && { rel: "noopener noreferrer" })}
>
{linkText}
</Link>
</Stack>
);
};
@@ -1,83 +0,0 @@
import MuiTooltip from "@mui/material/Tooltip";
import { useTheme } from "@mui/material/styles";
import { Info } from "lucide-react";
import type { TooltipProps } from "@mui/material/Tooltip";
type StyledTooltipProps = TooltipProps;
export const Tooltip = ({
placement = "top",
slotProps,
...props
}: StyledTooltipProps) => {
const theme = useTheme();
return (
<MuiTooltip
title={props.title}
placement={placement}
slotProps={{
tooltip: {
sx: {
backgroundColor:
theme.palette.mode === "dark"
? theme.palette.grey[900]
: theme.palette.grey[800],
color: theme.palette.common.white,
fontSize: "13px",
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: theme.shadows[6],
},
},
...slotProps,
}}
>
{props.children}
</MuiTooltip>
);
};
export interface TooltipWithInfoProps extends Omit<StyledTooltipProps, "children"> {
iconSize?: number;
iconColor?: string;
}
export const TooltipWithInfo = ({
iconSize = 14,
iconColor,
...props
}: TooltipWithInfoProps) => {
const theme = useTheme();
const defaultColor = theme.palette.text.secondary;
return (
<Tooltip {...props}>
<span
style={{
display: "inline-flex",
alignItems: "center",
cursor: "help",
}}
>
<Info
size={iconSize}
strokeWidth={1.5}
style={{
opacity: 0.7,
color: iconColor || defaultColor,
transition: "opacity 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "1";
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "0.7";
}}
/>
</span>
</Tooltip>
);
};
@@ -1,30 +0,0 @@
export * from "./Breadcrumb";
export * from "./PageHeader";
export * from "./BasePage";
export * from "./EmptyState";
export * from "./EmptyStateIllustration";
export * from "./Fallback";
export * from "./SkeletonCard";
export * from "./BulletPointCheck";
export * from "./StatusBox";
export * from "./BaseBox";
export * from "./Table";
export { DataTable as Table } from "./Table";
export * from "./StatusLabel";
export * from "./PulseDot";
export * from "./Dot";
export * from "./MonitorStatus";
export { default as Icon } from "./Icon";
export * from "./Tooltip";
export * from "./StatBox";
export * from "./BaseChart";
export * from "./Gauge";
export * from "./Tabs";
export * from "./SplitBox";
export * from "./TextLink";
export * from "./NoticeBanner";
export * from "./OfflineBanner";
export * from "./Avatar";
export * from "./StatusCodeLabel";
export * from "./StrategyBadge";
export * from "./StepProgress";
@@ -1,17 +0,0 @@
import i18n from "@/Utils/i18n.js";
import { useSelector } from "react-redux";
import { useEffect } from "react";
import type { RootState } from "@/store";
const I18nLoader = () => {
const language = useSelector((state: RootState) => state.ui.language ?? "en");
useEffect(() => {
if (language && i18n.language !== language) {
i18n.changeLanguage(language);
}
}, [language]);
return null;
};
export default I18nLoader;
Loaded 100 of 1251 files, more files were not shown because too many files have changed in this diff. Show more