Compare commits

...
Author SHA1 Message Date
gorkem-bwl f92dc835b7 fix(status page): apply brand color via a dedicated brand token
The status page appearance form saves a customer-picked brand color
(`statusPage.color`), but `StatusPageThemeProvider` never consumed it —
every rendered surface styled from `tokens.up`, so the picker had no
visible effect on the public page.

Rather than overriding `tokens.up` (the semantic OK/up status color,
which would let a red or orange brand color render the "all
operational" indicator in a red that reads as downtime), this
introduces a separate `brand` / `brandStrong` / `brandSoft` token
triple that is distinct from the up/OK color.

## Changes

- `themes/tokens.ts`: add optional `brand` / `brandStrong` / `brandSoft`
  fields on `StatusPageThemeTokens`. Themes leave them undefined; the
  provider falls back to the theme's `up*` values so nothing regresses
  before individual themes opt in.
- `themes/StatusPageThemeProvider.tsx`: add `brandColor` prop and a
  `resolveBrandTokens` helper. When a valid hex is provided it feeds
  `brand`, `brandStrong = darken(x, 0.2)`, `brandSoft = alpha(x, 0.15)`
  using MUI's shared `alpha`/`darken` (replaces the ad-hoc hex helpers).
  Invalid or empty values fall through cleanly.
- `Status/index.tsx`: pass `brandColor={statusPage.color}` only at the
  public route. The admin preview provider wraps an iframe whose
  document renders itself, so passing the prop there had no effect and
  is dropped.
- `themes/refined/styles.ts`: switch accent surfaces (hero logo, active
  chart-switch button, hardware pill, footer link) from `tokens.up` to
  `tokens.brand`. Uptime bars, status pills, and OK indicators stay on
  `tokens.up`. Other themes keep their current styling until a designer
  decides which surfaces should switch.

## Benefits

- Brand Color picker actually changes the rendered accent surfaces.
- The semantic "OK/up" status color is never repainted by the brand
  color, so operational-state indicators stay green regardless of the
  customer's brand.
- Reuses MUI's `alpha` / `darken` helpers already used across the
  client; no divergent color utilities.
- Small, incremental adoption: themes opt in at their own pace by
  reading `tokens.brand*` where they want the brand accent.
2026-07-13 15:52:12 +03:00
Alexander Holliday 0b0d412187 Merge pull request #3766 from bluewave-labs/fix/network-notice-loopback-detection
fix(infrastructure): trigger network 'no traffic' notice on interface name only
2026-07-10 19:50:02 -07:00
gorkem-bwl 77b817778b address review feedback: rename notice to match new trigger
The notice now triggers on interface names (loopback / container-bridge
only), not on zero traffic. Update the variable, i18n key, and English
copy to match:

- showNoTrafficNotice -> showLoopbackOnlyNotice
- pages.infrastructure.charts.labels.netNoTraffic ->
  pages.infrastructure.charts.labels.loopbackOnlyNotice
- English string rewritten to describe the loopback-only condition
  rather than "zero traffic on every interface"

Other locales still hold the old string under the old key and will fall
back to English until the translation pipeline picks up the new key.
Tracked separately.
2026-07-11 04:29:22 +03:00
Alexander Holliday eddb9a82a8 Merge pull request #3764 from bluewave-labs/fix/increase-json-body-limit
fix(server): raise express.json body limit to 10mb
2026-07-10 18:23:44 -07:00
gorkem-bwl 9459edce00 scope 10mb body limit to /monitors/import/json only
Address review feedback: keep the global express.json() at the default
100kb and mount a route-scoped 10mb parser in front of it for
/api/v1/monitors/import/json. Express matches middleware in registration
order, so the import route uses the larger parser and every other
endpoint keeps the tight default. No change in DoS surface for normal
traffic.
2026-07-11 04:22:42 +03:00
Alexander Holliday eaf1064413 Merge pull request #3763 from bluewave-labs/fix/status-page-custom-css-injection
fix(status-page): inject sanitized customCSS on the public page
2026-07-10 09:51:10 -07:00
Alexander Holliday dae6f9a570 Merge pull request #3769 from edobusy/docs/readme-italian
docs(readme): add Italian translation
2026-07-10 09:20:18 -07:00
edobusy 9fa45c5871 docs(readme): add Italian translation 2026-07-10 23:46:49 +10:00
gorkem-bwl 2810cd3562 fix(infrastructure): trigger network "no traffic" notice on interface name only
The notice at the top of the network charts was firing whenever the aggregated
per-second delta was zero across all interfaces. That is a false positive when
Capture is running on a real host but the polling interval is >= 1 minute and
the aggregation bucket is also 1 minute — first == last in the bucket, delta
is zero, but the interface has plenty of real traffic in its cumulative
counters.

Base the notice on interface names instead: only show it when the ONLY
interfaces Capture reports are loopback / container-bridge names (lo, lo0,
docker0). Real NICs (enp0s6, eth0, wlan0, tunN, vethN, brN, etc.) keep the
notice hidden even when the aggregation currently reports zero throughput.

The underlying zero-throughput bug (single-sample buckets producing zero
deltas) is tracked separately.
2026-07-10 03:16:42 +03:00
gorkem-bwl f4177baed5 fix(server): raise express.json body limit to 10mb
Express defaults to a 100kb JSON body, which is too small for realistic
imports through POST /api/v1/monitors/import/json (backups with many
monitors and their config easily exceed 100kb, returning
"request entity too large").

Raise the parser cap to 10mb. That covers real-world import payloads
while keeping the DoS surface bounded — a larger, streaming file-upload
route for imports remains desirable as a follow-up.
2026-07-10 00:06:31 +03:00
gorkem-bwl 5feb666bdb fix(status-page): inject sanitized customCSS on the public page
The customCSS field on status pages was persisted and validated
end-to-end but never rendered on the public page, so operator-supplied
styles had no effect.

Inject the string as a <style> tag on the public route (which the
admin preview iframe also renders), reusing the existing
cssReferencesExternalResource check so external-resource CSS
(@import, url(http…), //…) is dropped defensively on the client.

The admin preview iframe already points at the public URL, so this
one insertion covers both the live page and the in-app preview.
2026-07-09 21:55:25 +03:00
Alexander Holliday 8396f478c5 Merge pull request #3760 from bluewave-labs/fix/status-page-monitor-leak
node:20 => node:22
2026-07-07 10:17:31 -07:00
Alex Holliday bbfd963deb node:20 => node:22 2026-07-07 10:16:56 -07:00
Alexander Holliday fc0cda3da6 Merge pull request #3758 from bluewave-labs/fix/status-page-monitor-leak
fix: status page monitor leak
2026-07-07 09:59:28 -07:00
Alex Holliday 861d867a17 run formatter 2026-07-07 09:55:21 -07:00
Alex Holliday 8d7c322279 bump version and deps 2026-07-07 09:54:21 -07:00
Alex Holliday 68a9bece1e bump dependencies 2026-07-07 09:48:34 -07:00
Alex Holliday 4f82a11c66 update tests 2026-07-07 09:47:41 -07:00
Alex Holliday cc1814f507 whitelist monitor properties instead of blacklist for status pages 2026-07-07 09:47:29 -07:00
Alex Holliday 436c11bcba Merge branch 'develop' into fix/status-page-monitor-leak 2026-07-07 09:02:48 -07:00
Alexander Holliday d5ec2936ad Merge commit from fork
Advisory fix 1
2026-07-06 09:46:15 -07:00
Alex 937ffe4f3b add and update tests 2026-07-06 09:39:59 -07:00
Alex 0df71d6356 use re2 for regex 2026-07-06 09:39:43 -07:00
Alex adba25269c tighten regex validation 2026-07-06 09:39:28 -07:00
Alex 386a384ef6 add RE2 dependency 2026-07-06 09:39:11 -07:00
Alex Holliday 8bf5db0834 remove dead code and tests 2026-07-03 11:28:00 -07:00
Alexander Holliday 817d01c857 Merge pull request #3754 from bluewave-labs/fix/worker-crash-on-mongo-error
fix: worker crash on mongo error
2026-07-03 10:50:59 -07:00
Alex Holliday 3f30acdbdf add tests 2026-07-03 10:45:53 -07:00
Alex Holliday 80b604b9ff add global unhandled exception handler 2026-07-03 10:34:02 -07:00
Alex Holliday 9356b0d482 catch on recordFailure, add defenisve cathc to run job 2026-07-03 10:33:45 -07:00
Alexander Holliday 6699a83948 Merge pull request #3753 from bluewave-labs/chore/worker-deregistration-tests
add additional worker tests
2026-07-03 09:48:18 -07:00
Alex Holliday 900c039672 add additional worker tests 2026-07-03 09:41:18 -07:00
Alex a4ab7d662e bump version fix 2026-07-02 11:05:30 -07:00
Alex 807d7e10c3 type bulk op 2026-07-02 10:55:57 -07:00
Alex 39043f3945 migration fix 2026-07-02 10:43:22 -07:00
Alex 5a72e1e123 Merge branch 'develop' of github.com:bluewave-labs/Checkmate into develop 2026-07-02 10:30:05 -07:00
Alex 959b150ebf fix migratin 2026-07-02 10:29:15 -07:00
Alexander Holliday 552b7a037d Merge pull request #3743 from bluewave-labs/feat/helm
feat: Update helm charts
2026-07-02 10:13:28 -07:00
Alex 8671685cf9 bump version 2026-07-02 10:07:42 -07:00
Alex ff536f40a2 use version tag 2026-07-02 09:44:37 -07:00
Alex e3d63a1cb4 bump version 2026-07-02 09:11:26 -07:00
Alexander Holliday 105d7f0580 Merge pull request #3723 from Buco7854/feat/customcss-status-page
Feat/customcss status page
2026-07-02 09:11:02 -07:00
Alexander Holliday e2a1f02971 Merge pull request #3744 from Harshitbro-ctrlv/feat/status-page-minimal-theme
feat(status-page): add minimal theme
2026-07-02 08:59:55 -07:00
Alex b7f548ff15 bump version 2026-07-02 08:49:36 -07:00
Harshitbro-ctrlv 29d05c1e57 feat(status-page): add minimal theme 2026-07-02 12:08:17 +05:30
Alex Holliday 0ec77a5ab1 add alias for backward compat 2026-06-30 16:58:52 -07:00
Alexander Holliday 95eb75f416 Merge pull request #3741 from sanmaxdev/fix/maintenance-window-monitor-edits
test: cover maintenance window monitor edits
2026-06-30 16:46:40 -07:00
Alex Holliday 8f657dcd36 fix mongo namespace 2026-06-30 10:51:21 -07:00
Alex Holliday c0e33eb4fa skip general rate limiter on dev mode 2026-06-30 09:08:20 -07:00
Alex Holliday 754b0af3a3 update helm charts 2026-06-30 09:08:01 -07:00
Alex Holliday a39792cf0b add readiness and liveness checks to API 2026-06-29 14:14:42 -07:00
Alex Holliday 35c9618d59 server->api 2026-06-29 10:15:29 -07:00
Alex Holliday c6c41c2e3e bare sevice 2026-06-29 10:06:34 -07:00
Alex Holliday f58825d19f clean up env vars 2026-06-29 10:03:48 -07:00
Alex Holliday e4649f3d76 housekeeping 2026-06-29 09:43:37 -07:00
Alexander Holliday c7361666f5 Merge pull request #3742 from bluewave-labs/feat/worker-health
Feat/worker health
2026-06-29 09:20:07 -07:00
Alex Holliday 3f5707721f fix logging on server startup 2026-06-29 09:12:42 -07:00
sanmaxdev cc5f033d0d test: cover maintenance window monitor edits 2026-06-27 16:21:37 +00:00
Alex Holliday d2d0fb788e resolve shutdown race 2026-06-25 12:29:21 -07:00
Alex Holliday 9d969d67ca udpate tests 2026-06-25 11:55:40 -07:00
Alex Holliday 6e1c26b987 remove shadowing var 2026-06-25 11:55:28 -07:00
Alex Holliday ef353dcb36 bugfix, move reconcile to job scheduler 2026-06-25 11:47:22 -07:00
Alex Holliday b21ef60cda add a worker to the dev docker compose 2026-06-25 11:03:04 -07:00
Alex Holliday f43bf9ab3d add observability to worker and health server 2026-06-25 11:02:38 -07:00
Alex Holliday 48ca64ec6f add observability repo method 2026-06-25 11:02:18 -07:00
Alex Holliday e036c6077e add tests 2026-06-25 10:37:07 -07:00
Alex Holliday c9f0254aed start health server, update shutdown, update env config 2026-06-25 10:36:53 -07:00
Alex Holliday b3d9c5b94c add health server and metrics to db queue worker 2026-06-25 10:36:31 -07:00
Alex Holliday fd536541d7 add test for job scheduler drain 2026-06-25 09:16:07 -07:00
Alex Holliday 7a82c76d5c update tests 2026-06-24 12:20:21 -07:00
Alex Holliday 5fab2d0a30 inject buffer service, implement drain 2026-06-24 12:17:02 -07:00
Alex Holliday 860b2d6214 add a getHealth method to interface, stub out implementation 2026-06-24 12:16:37 -07:00
Alex Holliday 8579edd56e add a shutdown method to buffer service 2026-06-24 11:58:32 -07:00
Arnaud GRIMBERT 969d24e369 Merge branch 'develop' into feat/customcss-status-page 2026-06-24 20:27:57 +02:00
Buco7854 52b95ad121 fix: keep the admin status page preview iframe same-origin
The preview framed the page public URL, which for a custom domain is cross
origin and blocked by the admin document CSP frame-src. Point the iframe at the
same-origin public path while the frame chrome still shows the public URL.
2026-06-24 18:21:40 +00:00
Buco7854 ea23610b3a fix: mirror server custom CSS validation on the client
The form only enforced the length cap, so CSS with an external url() or @import
passed client validation and was rejected by the server. Add the same tokenizer
based check with the same error message so it is caught inline before submit.
2026-06-24 18:21:27 +00:00
Buco7854 a4cc322a55 feat: apply status page CSP to custom domains
Detect custom domains by comparing the request host to the app host and apply
the tightened CSP there too, not just on the public path. Run the middleware
before express.static so static-served root documents get the header, and keep
helmet after static so its default-src does not block custom domain API calls.
2026-06-24 18:21:16 +00:00
Alexander Holliday 2b9ff52f6b Merge pull request #3738 from bluewave-labs/feat/clean-worker-split
feat: clean worker split
2026-06-24 11:02:14 -07:00
Alex Holliday 0f230c4d70 update tests 2026-06-24 10:53:13 -07:00
Alex Holliday 74783fd738 clean up logger 2026-06-24 10:53:03 -07:00
Alex Holliday 71d3e789ff split services to api, worker, and shared 2026-06-24 10:30:21 -07:00
Alex Holliday f59a7c3113 extract shared services 2026-06-23 12:26:42 -07:00
Buco7854 ffbf223288 feat: tighten CSP on the public status page document
Layer a stricter Content-Security-Policy on the /status/public document so
custom CSS cannot load external images, fonts, or stylesheets. Browsers
enforce the intersection of all CSP headers, so this only narrows the public
status page and never loosens the global policy, while keeping the app's
Google Fonts. Browser-enforced backstop behind the validator.
2026-06-23 19:25:40 +00:00
Buco7854 96021b7479 refactor: detect external CSS references with the CSS Syntax tokenizer
Replace the regex/escape-normalizer with @csstools/css-tokenizer (already
present transitively). Tokenizing resolves comments, hex escapes, and line
continuations into the real value before scanning, so obfuscated forms (a
scheme split by a line continuation or hex escape) cannot slip past. Scan
url-tokens and string-tokens to cover url(), quoted @import, and string-form
image functions. Add regression tests for the bypasses.
2026-06-23 19:25:18 +00:00
Alex Holliday f0a5974eaf update tests 2026-06-23 11:31:35 -07:00
Alex Holliday c9b8f434cd split worker into a scheduler and worker 2026-06-23 11:22:37 -07:00
Buco7854 3685e0c476 feat: reject custom CSS that references external resources
Reject status page custom CSS containing @import or a url() / image-set()
/ image() / cross-fade() whose target is http(s) or protocol-relative,
while allowing data: and relative URLs. The check normalizes comments and
CSS escapes first so obfuscated forms cannot slip past. Enforced in the
create/update body validation.
2026-06-22 23:49:58 +00:00
Alexander Holliday d6f9fca907 Merge pull request #3737 from bluewave-labs/feat/head-request-fe
feat: head request front end
2026-06-22 11:36:02 -07:00
Alex Holliday ba943e90c2 add form UI 2026-06-22 11:24:35 -07:00
Alex Holliday dead97169f add default GET 2026-06-22 11:24:03 -07:00
Alexander Holliday eb1d307932 Merge pull request #3736 from bluewave-labs/feat/head-request-be
feat: head request back end
2026-06-22 11:19:34 -07:00
Alex Holliday a31dd2f19e update montior form hook to load method 2026-06-22 11:07:56 -07:00
Alex Holliday d84fc7e299 add method to validation 2026-06-22 10:48:39 -07:00
Alex Holliday 8afd67a06d add method to monitor type 2026-06-22 10:43:30 -07:00
Alex Holliday 71a4fddffd add tests 2026-06-22 10:38:15 -07:00
Alex Holliday 3489ee000d add super refine to reject method HEAD and advanced matching/json path 2026-06-22 10:31:13 -07:00
Alex Holliday 93b9c5033a add http metod option, add monitor.method guard to buildResponse 2026-06-22 10:26:10 -07:00
Alex Holliday 32ec909bf7 add default GET for imported monitors 2026-06-22 10:25:27 -07:00
Alex Holliday 8e5757beb1 add a method field to model, add enums and validation for http methods 2026-06-22 10:15:14 -07:00
Alexander Holliday 8e5a337f3e Merge pull request #3735 from bluewave-labs/fix/diagnostic-stats
fix: diagnostic stats
2026-06-22 10:01:25 -07:00
Alex Holliday 9db4e557b3 record last finished timestamp for failures 2026-06-22 09:58:11 -07:00
Alex Holliday 10174da4f0 update locales 2026-06-22 09:54:58 -07:00
Alex Holliday 99ded83bd5 remove dead fields from table, add correct fields 2026-06-22 09:54:49 -07:00
Alex Holliday dffa49ae9d remove dead fields 2026-06-22 09:54:17 -07:00
Alex Holliday 89c86a7628 remove dead fields from WorkerSummary 2026-06-22 09:53:50 -07:00
Alexander Holliday 09446c9fc1 Merge pull request #3733 from bluewave-labs/fix/excess-polling
Fix/excess polling
2026-06-22 09:26:36 -07:00
Alex Holliday 755134ee7f add poll backoff tests 2026-06-22 09:15:39 -07:00
Alex Holliday 3b919044e6 wake worker on job change 2026-06-19 10:25:59 -07:00
Alex Holliday 08e9c02a3a update tests 2026-06-19 10:02:06 -07:00
Alex Holliday 85f674ee08 resovle merge conflict 2026-06-19 09:55:52 -07:00
Alexander Holliday 16036f421b Merge pull request #3734 from bluewave-labs/fix/maintenance-window-exit
fix: change status on exit maintenance window
2026-06-19 09:54:05 -07:00
Alex Holliday 7fe7b79103 update server tests 2026-06-19 09:51:25 -07:00
Alex Holliday a058c0518c change status on exit maintenance window 2026-06-19 09:37:24 -07:00
Alex Holliday 5ed9ab780a update tests, add check producer tests 2026-06-18 11:06:57 -07:00
Alex Holliday e32590425b remove unsed from constructor 2026-06-18 11:06:43 -07:00
Alex Holliday 190a3b47da add findByIdLean for workers 2026-06-18 11:06:19 -07:00
Alex Holliday f740eef0fa thread prefetched worker, remove dead code 2026-06-18 11:05:57 -07:00
Alex Holliday 5efd64d44d thread prefetched monitor trough status service 2026-06-18 11:05:17 -07:00
Alexander Holliday b28c84436e Merge pull request #3718 from harsh-aghara/feat/accept-status-codes-frontend
Add customUpCodes support for HTTP monitors (Frontend)
2026-06-18 10:51:03 -07:00
harsh-aghara 9082cf0826 test(server): add comprehensive edge-case tests for customUpCodes validation 2026-06-18 22:51:21 +05:30
Alex Holliday 9d453bdb0b Merge branch 'develop' into fix/excess-polling 2026-06-18 10:19:57 -07:00
Alexander Holliday 39d92fb067 Merge pull request #3732 from bluewave-labs/fix/maintenance-rearm-jobs
rearm jobs on maintenance window pause/delete
2026-06-18 10:19:40 -07:00
Alex Holliday a54fdd89ab rearm jobs on maintenance window pause/delete 2026-06-18 10:17:33 -07:00
Alexander Holliday 6aa9360b98 Merge pull request #3731 from bluewave-labs/fix/evluation-order
fix: check evaluation order
2026-06-18 10:04:31 -07:00
Alex Holliday bb3f471d19 Merge branch 'fix/evluation-order' into fix/excess-polling 2026-06-18 09:58:47 -07:00
harsh-aghara 6d433fd0ac feat(client): polish customUpCodes ui 2026-06-18 22:27:38 +05:30
harsh-aghara d8dafa311a feat: sync custom http status codes between frontend and backend
- Add standard custom HTTP status codes (Cloudflare, AWS ELB, etc.) to both frontend and backend allowlists.
- Enforce strict validation of custom status codes using Zod sets on both sides, ensuring they match perfectly.
2026-06-18 22:27:38 +05:30
Alex Holliday 398e5d7314 add tests 2026-06-18 09:57:05 -07:00
Alex Holliday 53ee45172d remove evluation from worker 2026-06-18 09:56:56 -07:00
Alex Holliday 1ea6dafb1a evaluate checks on buffer flush 2026-06-18 09:56:38 -07:00
Alex Holliday 9785dc146e inject job repo into buffer service 2026-06-18 09:56:22 -07:00
harsh-aghara 1b1d8ea229 feat(client): implement accepted status codes UI in create monitor form 2026-06-18 21:37:14 +05:30
harsh-aghara 603bd1d814 feat(client): add customUpCodes to Monitor type and validation schema 2026-06-18 21:37:14 +05:30
harsh-aghara 158d976ccb feat(client): add HTTP status codes constants for monitor options 2026-06-18 21:37:14 +05:30
Alex Holliday 380fdf470f backoff excessive polling 2026-06-17 11:35:15 -07:00
Alexander Holliday 1e2a4b4a84 Merge pull request #3730 from bluewave-labs/feat/additional-mongo-stats
feat: additional mongo stats
2026-06-17 11:09:30 -07:00
Alex Holliday 5e0d8f0550 update tests 2026-06-17 11:01:56 -07:00
Alex Holliday bffc4ee813 add stats to fe 2026-06-17 10:55:49 -07:00
Alex Holliday ad9ff7f5df add mongo read/write per second stats 2026-06-17 10:55:39 -07:00
Alexander Holliday 8277c4ed13 Merge pull request #3728 from bluewave-labs/feat/db-queue
feat: db queue
2026-06-17 09:48:40 -07:00
Alex Holliday f65238da6c reformat 2026-06-17 09:45:59 -07:00
Alex Holliday 64b8aab4d6 repair tests 2026-06-17 09:40:51 -07:00
Alex Holliday 7244b35091 add upsertCleanup to schedule immediate cleanup jobs, cleanup orphaned jobs 2026-06-17 09:38:21 -07:00
Alex Holliday 699883e0b1 add jitter and tests 2026-06-16 11:34:58 -07:00
Alex Holliday f7211711ce add db queue worker tests 2026-06-16 10:59:56 -07:00
Alex Holliday cb0dd878d6 update .env.example 2026-06-16 10:59:46 -07:00
Alex Holliday 8c5be9ef00 removed pulsecron and ioredis 2026-06-16 10:50:50 -07:00
Alex Holliday d1b6d08c73 add tests 2026-06-16 10:50:39 -07:00
Alex Holliday 2d68e96782 drop jobs in migration 2026-06-16 10:50:29 -07:00
Alex Holliday 306f2a60a2 update tests 2026-06-15 12:22:55 -07:00
Alex Holliday abf6dc2908 single worker ID 2026-06-15 12:21:36 -07:00
Alex Holliday 7ce6068757 fix migration 2026-06-15 12:12:14 -07:00
Alex Holliday a7d9bb822e add tests, fix failing tests 2026-06-15 12:05:31 -07:00
Alex Holliday 142ecaaf8b handle renewing locks for long running jobs 2026-06-15 12:05:15 -07:00
Alex Holliday 90bbc764fc readd queue-worker domain, add heartbewat to db-queue 2026-06-15 11:26:49 -07:00
Alex Holliday ae4c433246 add a feature flag to toggle primary worker processing jobs or not 2026-06-15 11:02:46 -07:00
Alex Holliday d2461ef118 remove old queues 2026-06-15 10:39:48 -07:00
Alex Holliday 47162f2064 add a batch claim method 2026-06-12 16:04:31 -07:00
Alex Holliday 8b9c41464a dbQueueWorker 2026-06-12 15:43:36 -07:00
Alex Holliday 51b6ec1751 add check producer and evaluator 2026-06-12 12:36:50 -07:00
Buco7854 6165ae60e6 Merge branch 'develop' of https://github.com/bluewave-labs/checkmate into feat/customcss-status-page
# Conflicts:
#	server/src/api/routes/statusPageRoute.ts
2026-06-12 16:01:03 +00:00
Buco7854 69e142f711 fix: restrict status page mutations to admins and superadmins
Creating, updating and deleting status pages was only guarded by
verifyJWT, so any authenticated team member could set fields including
the new customCSS via a direct API call. The admin gating existed only
in the client UI (hidden buttons), and the create/configure routes had
no guard, so the form was reachable by URL. Enforce the role at the API
with isAllowed, matching the monitor routes.
2026-06-12 15:43:58 +00:00
Alex Holliday eff4f8ad69 fix method name 2026-06-11 15:20:13 -07:00
Alex Holliday bca902471d update tests 2026-06-11 15:19:53 -07:00
Alex Holliday 1b3f2f0365 add toStatusResponse to service, add findUnevaluated to repo 2026-06-11 15:19:45 -07:00
Alex Holliday be12991997 add tests 2026-06-11 14:19:21 -07:00
Alex Holliday 54596fe776 add mongodb-memory-server for testing 2026-06-11 14:19:14 -07:00
Alex Holliday 8d637dbcac update validation 2026-06-11 14:18:56 -07:00
Alex Holliday 771fd5b50f add migration for lastEvaluatedAt backfill 2026-06-11 14:18:44 -07:00
Alex Holliday 8dedf2a742 add last evaluated at field 2026-06-11 14:18:32 -07:00
Alex Holliday c882daf000 add job domain 2026-06-11 14:18:21 -07:00
Alexander Holliday 1aa7ae8f11 Merge pull request #3715 from Cincinnatus101010/feat/status-page-custom-domain
feat: custom domain support for published status pages
2026-06-11 12:31:20 -07:00
Ian Troisi 517139cbf4 fix: cache negative custom domain CORS lookups
Use a shorter TTL for rejected origins so unknown domains do not hit
the database on every cross-origin request.
2026-06-11 14:48:31 -04:00
Ian Troisi ef049b6521 fix: resolve CORS TypeScript error and format server files
Type defaultCorsOptions as CorsOptions so allowedHeaders is compatible
with the cors middleware. Format files from develop merge for CI.
2026-06-11 13:55:10 -04:00
Ian Troisi 33f8641f3a Merge branch 'develop' into feat/status-page-custom-domain 2026-06-11 13:54:27 -04:00
Alexander Holliday 2f22a52116 Merge commit from fork
Advisory fix 1
2026-06-11 10:44:42 -07:00
Ian Troisi 8af13b04ec fix: derive custom domain URL protocol from client host
Use the configured client host protocol instead of hardcoding https for
custom domain status page links.
2026-06-11 13:43:19 -04:00
Alex Holliday ac77216f07 add middleware tests 2026-06-11 10:42:15 -07:00
Alex Holliday a43363ebfc apply middleware 2026-06-11 10:42:05 -07:00
Alex Holliday 0ca738b080 add an upload type with size constraints and acceptable file types 2026-06-11 10:41:53 -07:00
Alex Holliday 091c36cbf3 create upload middleware 2026-06-11 10:40:59 -07:00
Ian Troisi 120708ed47 perf: cache custom domain CORS origin lookups
Avoid hitting the database on every cross-origin request by caching
published custom domain checks for five minutes.
2026-06-11 13:15:02 -04:00
Ian Troisi 6a1ab622b3 fix: scope custom domain CORS to public status page routes
Only allow registered custom domain origins on public GET/OPTIONS status
page API routes instead of the entire application API surface.
2026-06-11 13:15:02 -04:00
Ian Troisi 0aef8b652f fix: use partial index for status page customDomain
Replace the sparse unique index with a partial index so multiple status
pages without a custom domain no longer collide on null values.
2026-06-11 13:14:52 -04:00
Alexander Holliday 20863fbce7 Merge pull request #3725 from bluewave-labs/feat/decouple-worker
feat: decouple worker
2026-06-10 13:24:11 -07:00
Alex Holliday fa7eaa4788 update tests 2026-06-10 13:13:01 -07:00
Alex Holliday ee9c08b1bb extract reactors and pipeline 2026-06-10 13:12:51 -07:00
Alexander Holliday 7ee30e12f5 Merge pull request #3724 from bluewave-labs/chore/worker-reorg
chore: worker reorg
2026-06-10 09:34:31 -07:00
Alex Holliday c5f57eda62 update tests 2026-06-10 09:30:24 -07:00
Alex Holliday 2bf3c08f3c reorg jobqueue to worker 2026-06-10 09:30:14 -07:00
Ian Troisi 187c8eaba1 chore: format openapi.json with prettier 2026-06-10 11:03:34 -04:00
Buco7854 25d8839441 feat: render admin status page preview in an iframe
The preview previously rendered the themed status page inline in the
admin document, so document-global concerns leaked in both directions:
custom CSS spilled into the admin UI and the theme body painting never
showed. Pointing the BrowserFrame at the real public URL in an iframe
isolates the CSS completely and makes the preview pixel-identical to
what visitors see. Auth for unpublished pages keeps working because the
same-origin iframe shares localStorage and the API client reads the
token from the rehydrated store.
2026-06-10 17:02:18 +02:00
Buco7854 0f4240d3db feat: cap custom CSS length and add validation tests
Bound customCSS to 100000 characters on both the client schema and the
server body validation (the server is the enforcement boundary; multer's
1MB field cap was the only limit), regenerate the OpenAPI spec, and add
a validation test suite covering acceptance, clearing, the length cap,
and type rejection.
2026-06-10 17:02:17 +02:00
Ian Troisi a37bc63f31 Merge branch 'develop' into feat/status-page-custom-domain
Resolve conflicts from server reorganization while preserving custom
domain support for published status pages.
2026-06-10 10:55:59 -04:00
Buco7854 73f95ac8e1 feat: complete custom CSS support for status pages
The customCSS field existed on the model, types, and form schema but was
never editable, sent to the API, or rendered. Wire it end to end:

- Accept customCSS in the create/update status page body validation and
  regenerate the OpenAPI spec
- Add a Custom CSS editor (multiline field) to the status page form and
  include the value in the submitted form data
- Support multiline mode in the shared TextInput component
- Inject the saved CSS via a style tag when rendering the status page
2026-06-10 14:16:27 +00:00
Alexander Holliday c8c4bac2c7 Merge pull request #3719 from bluewave-labs/chore/organization
chore: organization
2026-06-09 12:40:06 -07:00
Alex Holliday 9fecfa562f update claude.md 2026-06-09 12:36:38 -07:00
Alex Holliday 7991f3f6a1 ignore unknown on format on client 2026-06-09 12:36:16 -07:00
Alex Holliday 692d4d89d6 update configs 2026-06-09 12:35:57 -07:00
Alex Holliday f1b104b78d ignore unknown on format 2026-06-09 12:35:19 -07:00
Alex Holliday 3c2b571313 update openapi docs 2026-06-09 12:34:57 -07:00
Alex Holliday abec4e6c70 update tests 2026-06-09 12:34:30 -07:00
Alex Holliday d022f7d849 reorg to domain and api 2026-06-09 12:34:15 -07:00
Alexander Holliday 0fb210e9e1 Merge pull request #3713 from Rabson/fix/diagnostics-totalsize-undefined
fix: (diagnostics) derive Mongo totalSize when dbStats omits it (Atlas shared tier)
2026-06-09 09:09:47 -07:00
Alexander Holliday ce4cb63035 Merge pull request #3690 from harsh-aghara/feat/accept-status-codes-backend
Add customUpCodes support for HTTP monitors (Backend)
2026-06-08 12:04:42 -07:00
harsh-aghara f637b01631 refactor: introduced helper method to remove redundant code 2026-06-08 17:30:26 +05:30
Ian Troisi e390bd764f Format openapi.json for CI format-check. 2026-06-07 22:42:58 -04:00
Ian Troisi 30e8cfc9ec Add custom domain support for published status pages.
Fixes #3647
2026-06-07 22:37:24 -04:00
Alexander Holliday 80d6868a1d Merge pull request #3714 from bluewave-labs/fix/status-page-styling
fix: status page styling
2026-06-07 15:23:06 -07:00
Alex Holliday 942d75afcd sm -> md 2026-06-07 15:21:53 -07:00
Alex Holliday c34fcc5cb3 fix responsiveness for status page themes 2026-06-07 15:18:22 -07:00
Alex Holliday 409f9f6dc6 fix status page styling 2026-06-07 10:54:58 -07:00
Alexander Holliday 525206c1c1 Merge pull request #3711 from bluewave-labs/feat/uptime-percentage
feat: uptime percentage resolves #3703
2026-06-07 10:18:56 -07:00
Alex Holliday 9a85389b8c update tests 2026-06-07 10:16:31 -07:00
Alex Holliday eeac784922 dispaly uptime percentage on status pages 2026-06-07 10:02:44 -07:00
Alex Holliday 19b873ca87 convert findByIds to aggregate pipeline to include uptime percentage 2026-06-07 10:02:25 -07:00
Alex Holliday 0f99c7004d remove unused import 2026-06-07 09:44:44 -07:00
Rabson 6a566c3693 fix(diagnostics): derive Mongo totalSize when dbStats omits it
MongoDB Atlas shared/free tiers return a legacy dbStats shape that omits
`totalSize` (while still returning storageSize/indexSize). diagnosticService
passed that undefined straight through, so the Diagnostics UI crashed with
"Expected a finite number, got undefined".

Derive totalSize (storageSize + indexSize) at the DB and per-collection level
so it always satisfies the non-optional `totalSize: number` type. Confirmed it
is not a version removal (stock mongo:8 returns totalSize) nor a permissions
issue (missing even with atlasAdmin). Adds unit tests for the derivation.
2026-06-07 08:58:09 +08:00
Alex dfcf0309fe add uptime percentage label 2026-06-06 16:08:35 -07:00
Alex 395b7fbe2a add uptimetime stats method and aggregation 2026-06-06 16:08:00 -07:00
Alexander Holliday 73b74ffd80 Merge pull request #3705 from bluewave-labs/fix/migrations
bump version
2026-06-05 12:02:09 -07:00
Alex Holliday 2b393f9ebe bump version 2026-06-05 12:01:35 -07:00
Alexander Holliday 562a17707d Merge pull request #3704 from bluewave-labs/fix/migrations
fix/migrations
2026-06-05 12:00:51 -07:00
Alex Holliday 7d800b0f38 Merge branch 'develop' into fix/migrations 2026-06-05 11:58:11 -07:00
Alex Holliday a19479b578 fix migration race condition 2026-06-05 11:58:00 -07:00
Alexander Holliday 9b4a810e21 Merge pull request #3700 from bluewave-labs/chore/version-bump
bump version
2026-06-05 10:38:05 -07:00
Alex Holliday 1388d61b07 bump version 2026-06-05 10:37:43 -07:00
Alexander Holliday 8dbe42288b Merge pull request #3684 from jhcpeixoto/feat/ntfy-notifications
feat: add ntfy notification support
2026-06-05 09:35:04 -07:00
harsh-aghara d2c13fac5a Merge remote-tracking branch 'upstream/develop' into feat/accept-status-codes-backend
# Conflicts:
#	server/src/service/infrastructure/network/HttpProvider.ts
2026-06-05 00:58:35 +05:30
harsh-aghara dd16ac060a fix: run advanced matcher on HTTP errors with custom up codes
When customUpCodes mark an HTTPError as up, parse the response body, apply advancedMatcher.validate, and set status to statusUp && matchResult.ok. Return parsed payload and extracted values to match the success path.
2026-06-05 00:35:30 +05:30
harsh-aghara 5628c202f7 refactor: extract isStatusUp utility to decouple from got's response.ok and modified utils test accordingly 2026-06-04 22:43:43 +05:30
Alexander Holliday 45e31e7212 Merge pull request #3694 from bluewave-labs/feat/fe/progress-indicator-infra
Feat: Progress indicator infra & pagespeed
2026-06-04 00:25:19 -07:00
Br0wnHammer 7872b18108 refactor(client): inline isEditMode instead of shadowing it with isWizard 2026-06-04 11:09:09 +05:30
Alexander Holliday 15ac5fa84d Merge pull request #3697 from bluewave-labs/fix/jobs-pagination
fix: jobs pagination
2026-06-03 13:32:47 -07:00
Alex Holliday 3c85b36b1b add pagnation validation to queue query 2026-06-03 13:30:03 -07:00
Alex Holliday ee0bc5e500 add pagination components 2026-06-03 13:29:17 -07:00
Alex Holliday 4729863826 add tests 2026-06-03 13:29:03 -07:00
Alex Holliday 174fb048f7 implement pagination, update metrics to use scheduler's aggregation 2026-06-03 13:28:53 -07:00
Alex Holliday 63fd3f6afd bump scheduler 2026-06-03 13:28:25 -07:00
Alex Holliday c3944f2fe7 Add pagination to controller 2026-06-03 13:28:15 -07:00
Alexander Holliday ddd937d42f Merge pull request #3696 from bluewave-labs/fix/connection-reuse
fix: connection reuse
2026-06-03 11:54:33 -07:00
Alex Holliday 5e225f9173 update tests 2026-06-03 11:51:42 -07:00
Alex Holliday d2d110fcd5 reuse http agent, use firstByte instead of total 2026-06-03 11:48:59 -07:00
harsh-aghara 372fc9869b refactor(monitor): address PR feedback for customUpCodes
Add HttpStatusCode types and simplify provider logic
2026-06-03 23:39:26 +05:30
Alexander Holliday 724f30070d Merge pull request #3695 from ajhollid/fix/less-simple-queue-tweaks
fix: less simple queue tweaks
2026-06-03 11:07:24 -07:00
Alex Holliday b8a44f09fc update tests 2026-06-03 11:04:13 -07:00
Alex Holliday f85f1aa1d1 add processEVery 2026-06-03 11:02:56 -07:00
Alex Holliday 707b8f903b add bulk ops for inserting monitors, set primary concurrency to 0, handles scheduling only 2026-06-03 11:00:40 -07:00
Alex Holliday 9e4fd6d708 bump scheduler 2026-06-03 10:59:28 -07:00
Br0wnHammer 8572065ef7 fix(client): validate hardware alert thresholds on the wizard's second step 2026-06-03 20:16:10 +05:30
Br0wnHammer 01c3870b22 feat(client): enable step wizard on infrastructure and pagespeed create pages 2026-06-03 20:15:01 +05:30
Alexander Holliday 420c103f41 Merge pull request #3693 from bluewave-labs/feat/worker-stats
feat: worker stats
2026-06-02 11:33:44 -07:00
Alex Holliday b4970d321d update tests 2026-06-02 11:31:22 -07:00
Alex Holliday f5a4a03898 reorder stop 2026-06-02 11:31:11 -07:00
Alex Holliday dfae6c8abd type metrics for super simple queue 2026-06-02 11:26:10 -07:00
Alex Holliday 2b6734bb78 update tests 2026-06-02 11:25:21 -07:00
Alex Holliday b9fd29b906 add compliant worker ID to super simple queue 2026-06-02 11:11:24 -07:00
Alex Holliday caf8f22fec add worker table 2026-06-02 10:59:14 -07:00
Alex Holliday 599c599588 add tests 2026-06-02 09:47:13 -07:00
Alex Holliday cfed235d1f add worker stats 2026-06-02 09:47:00 -07:00
Alex Holliday c9eacff71f add queue worker type 2026-06-02 09:46:23 -07:00
Alex Holliday b2526a5be9 init queue worker repo 2026-06-02 09:45:56 -07:00
Alex Holliday cffe3f57fa add models 2026-06-02 09:45:37 -07:00
Alex Holliday 8b2506cc3d add queue worker repository 2026-06-02 09:44:49 -07:00
Alex Holliday 60356a95fe bump less-simple-scheduler 2026-06-02 09:44:23 -07:00
Alexander Holliday 1195567da8 Merge pull request #3692 from bluewave-labs/feat/fe/progress-indicator-status-page
Feat: Progress indicator status page
2026-06-02 09:19:32 -07:00
Br0wnHammer 7617929700 feat(status-page): add step-wizard progress indicator to create flow 2026-06-02 20:31:01 +05:30
Br0wnHammer 7df3ee6ecd feat(status-page): add wizard step metadata and helpers to validation schema 2026-06-02 20:27:12 +05:30
Alexander Holliday dc788dbf85 Merge pull request #3691 from bluewave-labs/feat/workers
feat: workers
2026-06-01 11:24:11 -07:00
Alex Holliday 89c4f4005e add lessSimpleQueue tests 2026-06-01 11:21:49 -07:00
Alex Holliday 53ed3b3465 remove unused imports 2026-06-01 11:17:50 -07:00
Alex Holliday e37fbf2a5c replace empty monitor check 2026-06-01 11:11:36 -07:00
Alex Holliday 4df08bfa79 add worker scripts 2026-06-01 10:45:20 -07:00
Alex Holliday f74231cf33 early return if worker mode 2026-06-01 10:45:09 -07:00
Alex Holliday e1558d35b0 use queue type on config 2026-06-01 10:44:53 -07:00
Alex Holliday 51672890df add queue type to less simple queue 2026-06-01 10:44:39 -07:00
Alex Holliday fc357fe3ae add queue mode to env validation 2026-06-01 10:44:18 -07:00
Alex Holliday 1a0e6d9404 add queue mode type 2026-06-01 10:44:05 -07:00
harsh-aghara 87a6640c1d fix(backend): validate customUpCodes against standard HTTP status codes
Addresses reviewer feedback by enforcing strict validation on the  array. Previously, the Zod schema accepted any arbitrary number, which could allow invalid or self-defined HTTP codes (e.g., 5000) to be saved to the database.

Refs #3657
2026-06-01 13:21:30 +05:30
harsh-aghara 4e31499f78 feat(backend): add customUpCodes support for HTTP monitors
Adds a customUpCodes field to the monitor schema to allow users to specify which HTTP status codes should be treated as UP instead of DOWN. Includes core logic in HttpProvider and unit tests.

Refs #3657
2026-06-01 00:10:55 +05:30
Alexander Holliday b9e8a6dbc8 Merge pull request #3689 from bluewave-labs/fix/listener-leak
fix: move listener registration to constructor
2026-05-29 11:51:27 -07:00
Alex Holliday c704f67e57 move listener registration to constructor 2026-05-29 11:45:17 -07:00
Alexander Holliday 4629a947a4 Merge pull request #3688 from bluewave-labs/feat/less-simple-scheduler
feat: Configurable Schedulers
2026-05-29 11:41:32 -07:00
Alex Holliday da92b9d067 update tests 2026-05-29 11:33:31 -07:00
Alex Holliday 458442f364 fix env sample, fix log queue, use dbType for enum 2026-05-29 11:31:11 -07:00
Alex Holliday 400b295dae Merge branch 'develop' into feat/less-simple-scheduler 2026-05-29 11:24:51 -07:00
Alex Holliday 6ddee6e85b fix imports 2026-05-29 11:23:31 -07:00
Alex Holliday 2db88ac192 add lastRunAt back 2026-05-29 11:20:19 -07:00
Alex Holliday 0342870bbd lastRunAt -> lastFinishedAT 2026-05-29 11:08:32 -07:00
Alex Holliday 4280a6c2c3 update env vars 2026-05-29 10:34:42 -07:00
Alex Holliday 0a3232419a update prettierrc 2026-05-29 10:33:15 -07:00
Alex Holliday 4dc3a756a8 add less-simple-scheduler, bump dependencies 2026-05-29 10:32:58 -07:00
Alex Holliday cd91e83817 Remove old queues 2026-05-29 10:28:59 -07:00
Alex Holliday f476710c29 add queue type to settings service 2026-05-29 10:28:49 -07:00
Alex Holliday 0d255c8962 update imports 2026-05-29 10:28:18 -07:00
Alex Holliday bf25a65d8e add configurable queue selection to service config, default to in memory 2026-05-29 10:28:01 -07:00
Alex Holliday f1e16b65ee Extract job queue interfaces, add less simple queue 2026-05-29 10:27:38 -07:00
Alex Holliday 201ad987ce Add validation for Queue type 2026-05-29 10:27:03 -07:00
Alex Holliday c12ce2fbcf add queue types 2026-05-29 10:26:46 -07:00
Alexander Holliday 6e77738ecf Merge pull request #3687 from bluewave-labs/fix/readme-translations
docs: add README translations for all supported locales
2026-05-29 09:17:42 -07:00
gorkem-bwl cbd1153189 docs(readme): polish German translation — informal 'du', tighter phrasing, fewer AI tells 2026-05-29 12:11:56 -04:00
gorkem-bwl 88042e7c50 docs(readme): add machine-translation disclaimer to 7 less-confident translations
For ar, ca, cs, fi, th, uk, vi the AI-generated translation quality
is hardest for me to vouch for. Added a clear "machine-translated
draft — native-speaker PRs welcome" notice at the top of each so
the community can fix them. The note appears below the language
selector for visibility.
2026-05-29 12:09:40 -04:00
gorkem-bwl ba44985763 docs(readme): fix TOC anchors and refresh Spanish translation
## Anchor fix
GitHub auto-generates anchor IDs from the rendered heading text, so
the English TOC links (#users-guide, #installation, etc.) in the
translated READMEs were broken — the matching anchor IDs only existed
in the English README. Added hidden <a id="..."></a> markers before
each section heading in all 17 READMEs (root + 16 translations) so
the TOC works in every language.

## Spanish refresh
The existing Spanish README was a human translation of an older
revision of the project README — it carried its own Traducciones and
Patrocinadores sections, an outdated demo URL, and a different
feature list. Rewrote it from the current English README structure
while keeping a fluent Spanish tone.
2026-05-29 11:49:24 -04:00
gorkem-bwl b164fa63bf docs(readme): add README translations for all supported locales
Mirrors the Graphify pattern: a centered flag + native-language link
bar at the top of every README points to the rest. All translated
files live in docs/translations/ so the repo root stays clean.

## Coverage
- Added 15 new translations: ar, ca, cs, de, fi, fr, ja, pt-BR, ru,
  th, tr, uk, vi, zh-CN, zh-TW
- Moved README.es.md from the repo root to docs/translations/
- Updated the root README to include the language selector

The 17 languages here match the locales the Checkmate UI already
ships translations for, so README parity is consistent with what a
user sees inside the app.

## Quality note
Translations were drafted as a starting point and may need
native-speaker review for tone and idiom — especially Arabic, Thai,
the two Chinese variants, Japanese, and Vietnamese. Technical terms
(Docker, MongoDB, Redis, Webhook, etc.), URLs, contributor names,
and badge URLs are intentionally left as-is.
2026-05-29 11:09:05 -04:00
Alexander Holliday 7630491ad4 Merge pull request #3673 from bluewave-labs/feat/fe/uptime-progress-indicator
Feat: Uptime Progress Indicator
2026-05-28 09:13:40 -07:00
Alex Holliday 09be4cdf9a fix import, move cast 2026-05-28 09:10:11 -07:00
Br0wnHammer 8360c05448 refactor(client): derive wizard step count from Zod field metadata 2026-05-28 14:20:02 +05:30
Br0wnHammer 3cce1aa842 feat(client): split uptime monitor creation into a 3-step wizard 2026-05-28 00:21:02 +05:30
joaquimpeixoto 8a83bc99e9 feat: add ntfy notification support 2026-05-27 16:57:11 +01:00
Br0wnHammer 37dfc67e33 feat(client): split uptime monitor creation into a 3-step wizard 2026-05-27 12:07:07 +05:30
Alexander Holliday 4a939ad2c4 Merge pull request #3677 from bluewave-labs/feat/bump-concurrency
feat: bump concurrency
2026-05-26 15:48:43 -07:00
Alex Holliday 56ee8ef5ea bump concurrency to 50 2026-05-26 15:45:49 -07:00
Alexander Holliday 8dd13c4b75 Merge pull request #3676 from bluewave-labs/feat/mongo-diagnostics
reset page index
2026-05-26 15:14:17 -07:00
Alex Holliday 6b5c3bba47 reset page index 2026-05-26 15:13:36 -07:00
Alexander Holliday 02998ed06e Merge pull request #3675 from bluewave-labs/feat/mongo-diagnostics
Feat/mongo diagnostics
2026-05-26 15:13:10 -07:00
Alex Holliday 397541a789 extract strings 2026-05-26 15:09:33 -07:00
Alex Holliday 949beff4f9 add mongo collection stats component, reorder layout 2026-05-26 14:55:22 -07:00
Alex Holliday 806c7764b1 add slotprops prop to tooltip 2026-05-26 14:54:54 -07:00
Alex Holliday d18a53eec2 add forward ref to gauage for tooltip 2026-05-26 14:54:30 -07:00
Alex Holliday f4b14e7df9 update clinet side diagnostics type 2026-05-26 14:53:56 -07:00
Alex Holliday e834402672 tighten server types 2026-05-26 14:53:33 -07:00
Alex Holliday 290adf024c add mongo stats 2026-05-26 13:32:48 -07:00
Alex Holliday 2c89086b68 add mongo stats component 2026-05-26 13:32:30 -07:00
Alex Holliday 20167d537b update diagnostics type 2026-05-26 13:32:05 -07:00
Alex Holliday bf43632482 add mongo types 2026-05-26 13:31:44 -07:00
Alex Holliday 2d432e0a3d add mongo stats to diagnostic service 2026-05-26 13:31:13 -07:00
Br0wnHammer 967e415b0a feat(client): split uptime monitor creation into a 3-step wizard 2026-05-27 01:07:48 +05:30
Alexander Holliday 2764468e90 Merge pull request #3674 from bluewave-labs/chore/dependency-bump
chore: bump dependencies
2026-05-26 09:58:22 -07:00
Alex Holliday 10d0835eb3 bump dependencies 2026-05-26 09:56:35 -07:00
Br0wnHammer 1702fd1c5a fix(ui): react button dom 2026-05-25 23:41:55 +05:30
Br0wnHammer c98ec7d047 feat(client): split uptime monitor creation into a 3-step wizard 2026-05-25 23:35:51 +05:30
Br0wnHammer 9ffe0caed9 feat(client): add StepProgress segmented progress bar component 2026-05-25 23:31:02 +05:30
Alexander Holliday ef8a8016df Merge pull request #3672 from bluewave-labs/fix/tag-apperance-column
fix: add centering box for Tag labels
2026-05-25 10:42:43 -07:00
Alex Holliday 3a1a9bde4d add centering box 2026-05-25 10:40:22 -07:00
Alexander Holliday 0e24bb7c94 Merge pull request #3671 from bluewave-labs/fix/tests
fix: add tests for mw and monitor services
2026-05-25 09:59:24 -07:00
Alex Holliday a415c75eb4 add tests for mw and monitor services 2026-05-25 09:57:34 -07:00
Alexander Holliday d1f46aba9e Merge pull request #3669 from harsh-aghara/fix/checkbox-indeterminate-size
Add indeterminate icon to Checkbox component for consistency
2026-05-25 09:12:39 -07:00
Alexander Holliday bb60d22dea Merge pull request #3670 from harsh-aghara/fix/bulk-pause-toast-partial-failure
Expose failedCount and surface partial failure warning in UI
2026-05-25 09:11:10 -07:00
harsh-aghara 0ff642cb24 fix(ui): surface partial failure warning toast for bulk actions 2026-05-23 18:47:55 +05:30
harsh-aghara a044d7b2c7 fix(monitors): expose failedCount in bulk pause API response 2026-05-23 17:53:49 +05:30
harsh-aghara 9cee2eebd0 fix(ui): add missing indeterminate icon to Checkbox component
The custom Checkbox used Lucide Square/SquareCheck for unchecked/checked
states but had no indeterminateIcon, so MUI fell back to its default 24px
Material icon. This caused a visible size mismatch and row height jitter
in the bulk-select header of monitor tables.

Add SquareMinus from lucide-react at the same size (16px) and strokeWidth
(1.5) as the existing icons, and extend the checked color rule to also
cover the indeterminate state.
2026-05-22 23:28:42 +05:30
Alexander Holliday bec5ef09cb Merge pull request #3668 from bluewave-labs/fix/update-monitors-on-maintenance-window-change
fix: update monitors on maintenance window change
2026-05-21 11:18:59 -07:00
Alex Holliday 3abacb046b handle paused status 2026-05-21 11:02:59 -07:00
Alex Holliday fbfb861447 flip status on create 2026-05-21 10:50:53 -07:00
Alex Holliday 0470eab782 extract flipping monitor status, flip status on delete 2026-05-21 10:27:46 -07:00
Alex Holliday abf66eed95 add logic for entering/leaving maintenance windows, add tests 2026-05-21 10:23:27 -07:00
Alex Holliday 4f571a258c update scheduler to use extacted method 2026-05-21 10:02:56 -07:00
Alex Holliday 85196852f4 extract active maintenance window check 2026-05-21 10:02:39 -07:00
Alex Holliday 46c80985ec add findByIds to MW repository 2026-05-21 09:59:42 -07:00
Alex Holliday 8c7b691fda add updatebyids to monitor repository 2026-05-21 09:59:23 -07:00
Alexander Holliday d5dac6b557 Merge pull request #3663 from bluewave-labs/feat/fe-tags
Feat/fe tags
2026-05-20 12:29:02 -07:00
Alexander Holliday f0129f257a Merge pull request #3664 from bluewave-labs/fix/monitor-summary-with-tags
fix: monitor summary with tags
2026-05-20 12:28:46 -07:00
Alex Holliday 4b9f1ba43b add tags to summary query 2026-05-20 12:25:36 -07:00
Alex Holliday 1322033d41 add tags to infra monitors 2026-05-20 12:18:17 -07:00
Alex Holliday 7c201d087c add tag filtering to pagespeed 2026-05-20 12:14:22 -07:00
Alex Holliday 27e34f3b95 make filters optional 2026-05-20 12:14:09 -07:00
Alex Holliday 130d825d66 add active filters list 2026-05-20 12:02:45 -07:00
Alex Holliday c6d82223f9 add tags to filters 2026-05-20 12:02:32 -07:00
Alex Holliday 3bcdd3c445 remove chip referecnes, update strings 2026-05-20 11:39:57 -07:00
Alex Holliday 3f290bc963 add tags to Uptime montitor table 2026-05-20 11:37:03 -07:00
Alex Holliday 7d7926fab9 add tags to validation 2026-05-20 11:36:46 -07:00
Alex Holliday a09a885786 add tags to monitor type 2026-05-20 11:36:40 -07:00
Alex Holliday 76953f9a5c add tags to monitor forms 2026-05-20 11:36:23 -07:00
Alex Holliday 67bba1479c remove chip, add coloredlabel, add render options to autocomplete 2026-05-20 11:36:03 -07:00
Alex Holliday b981427d8d mutate SWR on success 2026-05-20 10:49:16 -07:00
Alex Holliday f37ae8fddb add menu string 2026-05-20 10:45:55 -07:00
Alex Holliday dc80b3cafc add tag to table 2026-05-20 10:45:45 -07:00
Alex Holliday 896d1b784b add a colored label 2026-05-20 10:45:36 -07:00
Alex Holliday 02640c3155 add routes 2026-05-20 10:29:25 -07:00
Alex Holliday 5a187cd1c9 add new chip component 2026-05-20 10:29:13 -07:00
Alex Holliday e6a0f89c68 add strings 2026-05-20 10:29:06 -07:00
Alex Holliday e96de364c0 add form hook for tags 2026-05-20 10:28:55 -07:00
Alex Holliday 8aa55885e5 add tags validaiton 2026-05-20 10:28:39 -07:00
Alex Holliday 1f7344f2e6 add Tags pages 2026-05-20 10:28:26 -07:00
Alex Holliday fe74d4b465 add tag type 2026-05-20 10:28:07 -07:00
Alexander Holliday 9c62dc9c52 Merge pull request #3661 from bluewave-labs/feat/monitor-tags
feat: monitor tags
2026-05-20 09:20:18 -07:00
Alex Holliday 2a2f101d4b add tests and validation 2026-05-19 14:45:42 -07:00
Alex Holliday 76ce362eea add tags to monitor controller 2026-05-19 14:45:29 -07:00
Alex Holliday 029f732a86 add tags to monitor service 2026-05-19 14:44:56 -07:00
Alex Holliday a7abbd60bd add tags to monitors repo 2026-05-19 14:44:18 -07:00
Alex Holliday 9b84b98391 add tests 2026-05-19 11:16:25 -07:00
Alex Holliday 6c2e3b4fe1 init tags 2026-05-19 11:16:16 -07:00
Alex Holliday 57acbd3497 add validations 2026-05-19 11:16:03 -07:00
Alex Holliday 48735c7dc8 add tag service 2026-05-19 11:15:54 -07:00
Alex Holliday 451c752170 add tags controller and routes 2026-05-19 11:15:42 -07:00
Alex Holliday b1871b31a5 create tags repository, update related repositories 2026-05-19 11:15:16 -07:00
Alex Holliday a740a9e005 update DB models, create tag db model 2026-05-19 11:14:55 -07:00
Alex Holliday 43ff7ca4bd create new tag type, update related types 2026-05-19 11:14:31 -07:00
Alexander Holliday 01a53298c2 Merge pull request #3655 from bluewave-labs/fix/chart-size
fix: width for stats size, noWrap for stats
2026-05-15 11:30:08 -07:00
Alex Holliday 3de392c3c9 fix width for stats size, noWrap for stats 2026-05-15 11:27:48 -07:00
Alexander Holliday d1b5830c3d Merge pull request #3654 from bluewave-labs/fix/set-password
add password and newPassword to editUserValidation
2026-05-15 11:16:34 -07:00
Alex Holliday 704f25b987 extract password validation, add regression test 2026-05-15 11:14:39 -07:00
Alex Holliday f46c2edd1b add password and newPassword to editUserValidation 2026-05-15 10:58:38 -07:00
Alexander Holliday 9d7523c406 Merge pull request #3653 from bluewave-labs/fix/locale-gaps
fix(i18n): close translation gaps across 16 locales
2026-05-15 10:46:57 -07:00
gorkem-bwl bc7b3a5ffb fix(i18n): close translation gaps across 16 locales
## Changes
- Add 59 missing keys to every non-en locale (DNS monitor strings,
  status-page themes, monitor status labels, network error messages,
  email sender display name)
- Catalan: add 12 bulk-monitor selection keys that were introduced after
  ca.json was first added
- Fill 188 empty string values across ar, cs, fi, fr, ja, pt-BR, tr,
  uk and vi
- Remove 10 orphan keys from es.json that were not referenced anywhere
  in the client code

## Benefits
- All 16 locales now report 100% structural coverage against en.json
- Status page theme picker, DNS monitor type, and bulk monitor actions
  are fully translated
- No more empty strings rendered as blank labels in production UI
- es.json no longer carries dead keys diverging from the reference

## Notes
- Placeholders ({{count}}, {{value}}, {{name}}) verified intact in
  every locale
- Brand terms (DNS, A, AAAA, CNAME, MX, NS, TXT, 8.8.8.8) intentionally
  kept untranslated
- Build, prettier format-check and JSON validity all pass
2026-05-14 07:36:37 -04:00
Alexander Holliday 72f23ca025 Merge pull request #3547 from ZeinebMegaadi/fix/maintenance-recurring-edit-validation
Fix/maintenance recurring edit validation
2026-05-13 11:41:38 -07:00
Alexander Holliday ae9416641c Merge pull request #3652 from bluewave-labs/fix/findByIdsWithChecks
fix: update query
2026-05-13 11:29:02 -07:00
Alex Holliday b7844f8b10 update query 2026-05-13 11:26:25 -07:00
Alexander Holliday ffb563f36d Merge pull request #3650 from bluewave-labs/feat/fe/maintenance-window-groups
Feat/fe/maintenance window groups
2026-05-12 11:21:05 -07:00
Alexander Holliday ae5115a38f Merge pull request #3649 from bluewave-labs/feat/maintenance-window-groups
feat: maintenance window groups
2026-05-12 11:20:51 -07:00
Alex Holliday b925434078 tighten validation, add tests 2026-05-12 11:18:51 -07:00
Alex Holliday f713698572 update maintenacne window hook and form 2026-05-12 11:06:26 -07:00
Alex Holliday 47d2b62e92 update type 2026-05-12 10:58:03 -07:00
Alex Holliday 780189e4a1 add migrations 2026-05-12 10:53:40 -07:00
Alex Holliday f86ceae0c6 update tests 2026-05-12 10:41:30 -07:00
Alex Holliday e78524d1c9 udpate service to use monitorIds instead of monitorId 2026-05-12 10:39:05 -07:00
Alex Holliday 7a2dab6cc4 update repository toEntity method 2026-05-12 10:38:47 -07:00
Alex Holliday c90442b49f update maintenance window type to match DB model 2026-05-12 10:38:22 -07:00
Alex Holliday bdecc1a5e2 update maintenance window model to use array of IDs instead of ID 2026-05-12 10:37:59 -07:00
Alexander Holliday c1fc537479 Merge pull request #3648 from bluewave-labs/fix/histogram-response-time
fix: histogram-response-times
2026-05-12 09:52:49 -07:00
Alex Holliday 6f0540fb33 properly type payload, avgResponseTime -> originalAvgResponseTime 2026-05-12 09:41:58 -07:00
Alexander Holliday 3876aae484 Merge pull request #3645 from bluewave-labs/fix/husky-merge-guard
fix: husky guard pre-commit against merge/rebase state
2026-05-11 11:39:36 -07:00
AlexandClaude Opus 4.7 2f646ee154 fix(husky): guard pre-commit against merge/rebase state
lint-staged uses git stash internally to isolate staged files for
linting. During a merge/rebase/cherry-pick/revert, that stash machinery
interacts catastrophically with MERGE_HEAD and friends — on any lint
failure it resets, silently wiping MERGE_HEAD and your conflict
resolutions. The user ends up in a loop where every merge attempt
leaves a clean tree with no merge commit.

This adds an early-exit guard for the four mid-operation states, and
also anchors the script to the repo root + scopes each lint-staged run
in a subshell so a subdir commit or per-package failure can't poison
the cwd for the next step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:37:23 -07:00
Alexander Holliday 01da5847f0 Merge pull request #3470 from bluewave-labs/feat/pagespeed-mobile-strat
feat: adding mobile/desktop pagespeed strategy
2026-05-11 11:34:11 -07:00
Alex 4b888c1ed1 bump dependencies 2026-05-11 11:30:58 -07:00
Alex 6b63091d89 tighten validation, add tests 2026-05-11 11:28:39 -07:00
Alex 78fdc548bb Merge remote-tracking branch 'origin/develop' into feat/pagespeed-mobile-strat 2026-05-11 11:11:13 -07:00
Alexander Holliday bbdbe75255 Merge pull request #3584 from Iriome-Santana/fix/non-root-container-support
fix: allow mono and arm containers to run as non-root user (UID 1000)
2026-05-11 10:38:42 -07:00
Alexander Holliday 74bc5accb6 Merge pull request #3644 from bluewave-labs/feat/fe/dns-monitors
feat: fe/dns monitors
2026-05-11 10:32:39 -07:00
Alex 60770a4544 udpate monitor path map 2026-05-11 10:29:59 -07:00
Alex 766d847561 tighten validation, replace negative type checks with allowed list 2026-05-11 10:27:41 -07:00
Alex 08043a5ea0 tighten validation, add tests 2026-05-11 10:12:27 -07:00
Alex 272e4b5014 update server services to support DNS type queries 2026-05-11 10:01:42 -07:00
Alex 98d2bbfbb9 add DNS monitor to create monitor form 2026-05-11 10:01:23 -07:00
Alex 427f8c632f update validation to include dnsServer and dnsRecordType 2026-05-11 10:01:04 -07:00
Alex 9a004ab1fb udpate monitor types to include DNS, add dnsRecordType 2026-05-11 09:59:53 -07:00
Alexander Holliday c9b9c4a53f Merge pull request #3643 from adosaiguas/develop
i18n: add Catalan language
2026-05-09 09:41:08 -07:00
Agustí Dosaiguas db746b0ff2 Merge branch 'bluewave-labs:develop' into develop 2026-05-09 18:15:31 +02:00
Agustí Dosaiguas 80fca45ce8 i18n: add Catalan language 2026-05-09 18:13:45 +02:00
Alexander Holliday 182ff390e9 Merge pull request #3642 from bluewave-labs/feat/dns-provider
feat: dns provider
2026-05-08 13:17:52 -07:00
Alex fc711e8e24 per call resolver 2026-05-08 13:15:45 -07:00
Alex 0f662e1a65 update tests 2026-05-08 13:04:18 -07:00
Alex 4b4fae91c2 use enums for validation 2026-05-08 12:56:56 -07:00
Alex 172d36ba1d use injected Resolver 2026-05-08 12:56:47 -07:00
Alex 0c36c8e625 tests 2026-05-08 12:44:58 -07:00
Alex 2a7bde9574 update toEntity methods to extract dnsServer and dnsRecord 2026-05-08 12:44:44 -07:00
Alex cb9cc101c0 add dnsServer and dnsRecordType to monitor model 2026-05-08 12:44:18 -07:00
Alex 06ef850577 update validation to allow for dnsServer and dnsRecord 2026-05-08 12:43:33 -07:00
Alex 0797008ac9 add dns record type 2026-05-08 12:43:02 -07:00
Alex Holliday fda9d9edfe add tests 2026-05-08 08:59:31 -07:00
Alex Holliday c6d86d78b8 Add DNS provider 2026-05-08 08:59:25 -07:00
Alex Holliday 0d72071019 add dns types 2026-05-08 08:59:11 -07:00
Alexander Holliday 84533119c9 Merge pull request #3640 from bluewave-labs/fix/maintenance-window-validation
fix(maintenance): reject past-dated one-time windows at creation
2026-05-07 11:44:13 -07:00
Alex 60cded78a6 validation tests 2026-05-07 11:43:00 -07:00
gorkem-bwl c45bcfaa2f revert(maintenance): keep one-time TTL cleanup, narrow PR to validator-only
Per review feedback: auto-deleting completed one-time maintenance windows is
intentional cleanup, not a bug. The actual bug is past-dated end values being
accepted at creation time, which the validator refines now reject up front.

Reverts:
- Restore expiry field + TTL index on MaintenanceWindow schema
- Restore repository's auto-set of expiry = end for repeat=0
- Drop migration 0007 (no longer needed)
- Restore expiry properties in openapi.json
- Restore expiry in create/edit validators (still optional)

Kept from this PR:
- .strict() on create/edit body schemas
- end > start refine on create/edit
- end > now refine on create when repeat=0 (the real fix)
- DurationUnits const tuple + reuse (rules 5/8/10)
- Client no longer sends redundant expiry (server still derives it)
2026-05-07 13:38:10 -04:00
Alexander Holliday 747f31477d Merge pull request #3638 from bluewave-labs/feat/status-code-down-label
feat(checks): render status code 5000 as "Down" with subtype tooltip
2026-05-07 09:58:26 -07:00
Alex 713c3ea34f add span for ref 2026-05-07 09:56:56 -07:00
Alex 1d90b8d8d9 refactor all call sites 2026-05-07 09:54:58 -07:00
Alex 0827d4f528 add new component to barrel export 2026-05-07 09:54:50 -07:00
Alex 7c14d81a24 refactor utils 2026-05-07 09:54:22 -07:00
Alex 7906f2f615 add stauts code label component 2026-05-07 09:54:03 -07:00
gorkem-bwl b462ba7562 refactor(maintenance): centralize DurationUnit + sync OpenAPI spec
Convention pass per docs/coding-conventions.md (rules 5/8/10/11):
- Promote DurationUnit from a raw union type to a const tuple + derived type.
- Mongoose schema enum imports DurationUnits instead of inlining the values.
- Both validators import DurationUnits instead of inlining z.enum twice.
- Drop stale 'expiry' entries from openapi.json (spec is now strict-mode-correct).
2026-05-07 12:38:54 -04:00
gorkem-bwl bd059da94a refactor(maintenance): tighten validator + migration per /simplify
- Collapse two refines into a single superRefine in createMaintenanceWindowBodyValidation; same on edit.
- Match peer migration convention: use mongoose.connection.db directly, not Model.collection.
- Tighten TTL index detection: require expireAfterSeconds + single-field key.
- Add Starting log at migration top to match peers 0003-0006.
2026-05-07 12:33:03 -04:00
gorkem-bwl 4db95cfa44 fix(maintenance): stop silent deletion of one-time maintenance windows
Maintenance windows could disappear seconds after creation when the API received
a payload with end dates already in the past. The repository auto-set
expiry = end for one-time windows (repeat = 0), and Mongo's TTL index on the
expiry field then deleted the row on its next sweep. This was particularly
visible with automation tools sending payloads in non-UTC timezones.

## Changes

### Validator hardening
- Add .strict() to create + edit body schemas so unknown keys return 400
  instead of being silently dropped.
- Refine end > start on create.
- Refine end > now for one-time create payloads (repeat = 0).
- Refine end > start on edit when both are provided.
- Remove the accepted expiry field from both schemas.

### Stop the auto-delete behaviour
- Repository.create no longer copies end into expiry.
- Schema's expiry field + TTL index removed.
- Migration 0007 drops the live TTL index and unsets stale expiry values on
  existing rows so legacy data is no longer at risk.

### Client
- Form submission no longer includes expiry.
2026-05-07 12:29:26 -04:00
gorkem-bwl 602769b3e7 feat(checks): render status code 5000 as "Down" with subtype tooltip
Status code 5000 is Checkmate's internal sentinel for "no HTTP response
received" (DNS failure, connection refused, timeout, TLS error, etc.).
Showing the raw number to admins is confusing — the value isn't a real
HTTP code and means nothing without context.

This change replaces "5000" with "Down" across all check/incident
surfaces, and adds a hover tooltip that explains the underlying cause
based on the stored error message. Real HTTP codes (2xx/3xx/4xx/5xx)
are unchanged.

## Changes
- Add client/src/Utils/statusCode.ts with formatStatusCode,
  getStatusCodeTooltip, getStatusCodeValueType, extractErrorCode, and a
  message-to-subtype parser (timeout / dns / refused / tls / reset).
- Wire into 6 render sites: Checks table, Uptime details Checks table,
  GeoChecks table + map popup, Incidents table, Incident details dialog.
- Add common.statusCode.* i18n keys in en.json with subtype-specific
  descriptions. Tooltip appends the raw error code (ECONNREFUSED,
  ETIMEDOUT, CERT_HAS_EXPIRED, etc.) when present in the message.

## Tooltip examples
- "queryA ECONNREFUSED www.example.com" →
  "The server refused the connection (port closed or service not listening). (ECONNREFUSED)"
- "connect ETIMEDOUT 1.2.3.4:443" →
  "The server didn't respond in time (request timed out). (ETIMEDOUT)"

## Notes
- No DB or server changes; statusCode field stays 5000 in storage.
- Other locales fall back to English keys until translated.
2026-05-07 01:13:09 -04:00
Gorkem Cetin a69f8a5ab5 Merge pull request #3637 from bluewave-labs/docs/coding-conventions
docs: rename frontend-conventions to coding-conventions, add backend rules
2026-05-06 14:06:53 -04:00
gorkem-bwl 940fc38056 docs: rename frontend-conventions to coding-conventions, add backend rules
The conventions doc grew up as an FE-only thing. After the DNS provider
PR series highlighted real backend patterns that get repeatedly missed
in code review (DI, AppError envelope, schema enum, test setup style,
centralized Zod enums, OpenAPI response schemas), it makes sense to
have a single doc that covers both halves of the codebase.

Changes:

- Rename docs/frontend-conventions.md → docs/coding-conventions.md.
- Add a new top-level rule 0: look at peer implementations and follow
  the established pattern. Applies to both FE and BE. Includes a
  4-step "how to apply" and a self-check for PR authors.
- Keep the 5 frontend rules unchanged (rules 1-5).
- Add 6 backend rules (rules 6-11):
  - Controller → Service → Repository → Mongoose layering.
  - Provider skeleton (DI, SERVICE_NAME / TIMEOUT_MS, timeRequest,
    inline setTimeout race, outer AppError catch).
  - Mongoose enum reusing types/* const tuples.
  - Provider test conventions (file location, @jest/globals, contract
    test, inline setup() per test).
  - Centralize Zod enums in types/* — don't duplicate.
  - New entity fields must land in *ResponseSchema for OpenAPI.
- Pre-PR checklist split into Universal / Frontend / Backend.
- CLAUDE.md updated to reference the new file and summarize all 11
  rules + rule 0.
2026-05-06 13:55:58 -04:00
Alexander Holliday bf369aa546 Merge pull request #3626 from harsh-aghara/feat/bulk-pause-resume
feat/bulk pause resume
2026-05-06 09:15:30 -07:00
harsh-aghara c72287cb21 fix(monitor): Resolve build fail.
Fixes #3602
2026-05-06 00:09:29 +05:30
harsh-aghara ba2d23fd82 refactor(monitors): optimize bulkTogglePause from 3 to 2 DB queries
Use find + updateMany with timestamps:false instead of find IDs +
updateMany + fetch updated. Manually set updatedAt and edit
in-memory documents to avoid a third DB round trip.

Fixes #3602
2026-05-05 23:43:06 +05:30
harsh-aghara e725128b10 feat(monitors): track and report bulk action queue failures
Addresses PR feedback:
- Upgrades bulk pausing from Promise.all to Promise.allSettled
- Logs individual job queue sync failures with monitor ID context
- Surfaces failedCount to the frontend via controller response
- Adds unit tests to verify partial failure handling

Fixes #3602
2026-05-05 17:08:25 +05:30
harsh-aghara baf7344e2d fix(ui): enforce strict type safety in bulk action error handling
Addresses PR feedback:
- Replaces forbidden any type in catch block with unknown
- Adds type discrimination using axios.isAxiosError and instanceof Error
- Narrows error type for logger.error compatibility

Fixes #3602
2026-05-05 17:07:13 +05:30
harsh-aghara 387cb35494 refactor(ui): simplify table selection and clarify mobile label rendering
Addresses PR feedback:
- Removes over-abstracted useTableSelection hook, inlines selection logic
- Replaces triple-condition with explicit hideMobileLabel bool flag
- Removes unsafe Record<string, never> casting

Fixes #3602
2026-05-05 17:06:48 +05:30
Alexander Holliday 6a8f97ae65 Merge pull request #3633 from bluewave-labs/fix/queue-table
fix: queue table
2026-05-04 12:19:39 -07:00
Alex Holliday 3365a8f7da extract colors to theme 2026-05-04 12:11:57 -07:00
Alex Holliday 2e9b87ba32 normalize tabqueue 2026-05-04 11:59:02 -07:00
Alex Holliday 1fd56610ea update tests 2026-05-04 11:55:39 -07:00
Alex Holliday 22230b8a08 UI fixes for job queue table 2026-05-04 11:53:12 -07:00
Alex Holliday f648ab1b24 add geoInterval, monitorActive, and repeat to jobs data 2026-05-04 11:52:45 -07:00
Alexander Holliday 281fc99136 Merge pull request #3632 from bluewave-labs/fix/pause-resume-geo-jobs
fix: pause resume geo jobs
2026-05-04 10:59:48 -07:00
Alex Holliday 6f0cc307dc log error on geo check pause/resume fail, update tests 2026-05-04 10:51:53 -07:00
Alex Holliday cb218a46a9 fix pause/resume logic 2026-05-04 10:21:28 -07:00
Alexander Holliday 39807c2de1 Merge pull request #3629 from bluewave-labs/feat/infrastructure-form-helper-text
feat(infrastructure): clarify capture endpoint and authorization secret on monitor form
2026-05-02 10:48:33 -07:00
Alexander Holliday 59d97f0d25 Merge pull request #3630 from bluewave-labs/fix/status-page-timezone-defaults
fix(status-page): use operator-configured timezone on public pages
2026-05-02 10:47:44 -07:00
harsh-aghara 657a35e948 feat(i18n): implement strict pluralization for bulk action notifications
Removes generic '(s)' syntax from backend responses. Adds proper _one and _other pluralization keys to all 16 locales with native grammar translations, resolving 0-count edge cases.

Fixes #3602
2026-05-02 14:28:15 +05:30
harsh-aghara 1b0b17b589 fix(ui): correct table header alignment glitch caused by checkbox injection
Adds explicit 'align' support to Table headers to override strict index-based alignment. Restores exact center-alignment spacer boxes to Uptime and Infrastructure tables to match develop branch styling.

Fixes #3602
2026-05-02 14:28:13 +05:30
harsh-aghara ff34c872c8 refactor(ui): convert BulkActionsBar to modular bottom pill using children pattern
Resolves maintainer feedback regarding UI/UX layout and prevents code collisions with other PRs by extracting action logic out of the bar component.

Fixes #3602
2026-05-02 14:28:12 +05:30
gorkem-bwl 1458cf4e4a fix(status-page): use operator-configured timezone on public pages
Anonymous visitors to a public status page have no persisted Redux
state, so ThemedHeatmap and ThemedHistogram tooltips always rendered
timestamps in the hardcoded "America/Toronto" default — regardless of
the timezone the operator configured. Even on the same domain,
ThemedHeatmap and ThemedHistogram had no code path to consume the
operator-configured statusPage.timezone returned by the API.

- Add SYSTEM_TIMEZONE module constant and a small resolveTimezone()
  helper to TimeUtils.ts. formatDateWithTz's timezone parameter is now
  optional and defaults to the system tz, so callers don't need to
  rebuild the prop -> browser -> UTC chain inline.
- Add a `timezone` field to StatusPageThemeContextValue so any themed
  component on the public page can read the page-wide tz without prop
  drilling (parallels how `theme` and `mode` are already exposed).
- Wire statusPage.timezone into StatusPageThemeProvider from
  Pages/StatusPage/Status/index.tsx (both the public route and the
  admin preview).
- ThemedHeatmap and ThemedHistogram now read tz from the context
  instead of useSelector(state.ui.timezone) — that selector was
  unreliable for anonymous visitors and removing it cuts a Redux
  subscription on the public page.
- Replace the inline `data?.timezone || Intl.DateTimeFormat()...`
  expression in useStatusPageForm.ts with resolveTimezone().

Closes #3628.
2026-05-01 22:28:12 -04:00
gorkem-bwl cb285bbb66 i18n: machine-translate new captureUrl key and hardware description
Translates captureUrl.label ("Capture endpoint") and the new long
description.hardware copy into all 15 non-English locales (ar, cs, de,
es, fi, fr, ja, pt-BR, ru, th, tr, uk, vi, zh-CN, zh-TW). The literal
URL example and the token name "Bearer" are kept untranslated.
2026-05-01 22:06:03 -04:00
gorkem-bwl 13b13b6818 i18n: stop clobbering translated hardware description
The previous commit overwrote description.hardware with English in 15
non-English locales. Restore each locale's existing translation; only
the en.json copy carries the new path/Bearer guidance. Translators will
pick up the English text on the next PoEditor sync.
2026-05-01 21:51:31 -04:00
gorkem-bwl 63c1a60b06 refactor(infrastructure): move capture-form guidance to ConfigBox subtitle
The previous commit added inline helperText under the URL and Secret
fields, but no other form in the codebase uses helperText for static
guidance — only for validation errors. The project's pattern for setup
guidance is the ConfigBox subtitle text on the left side of the form,
populated from pages.createMonitor.form.general.description.<type>.

Move the guidance there so this form follows the same convention as
every other monitor type.

- Remove urlHelperText / secretHelperText from GeneralSettingsConfig
- Remove the new helperText props on the URL and Secret TextFields
- Drop captureUrl.helperText and secret.helperText keys from all
  16 locales
- Beef up description.hardware to mention the /api/v1/metrics path
  requirement and the no-Bearer-prefix rule

The realistic captureUrl placeholder (added in the previous commit)
stays — it does most of the URL-disambiguation work on its own.
2026-05-01 18:01:44 -04:00
Iriome 53c58eb945 fix: make non-root support opt-in, keep root as default
Remove USER node directive so existing deployments are unaffected.
The chown ensures --user 1000 works without changing the default behaviour.
2026-05-01 21:56:40 +01:00
gorkem-bwl d5cd957bbd feat(infrastructure): add helper text to capture endpoint and secret fields
Two real users hit the same confusion: the Capture endpoint URL needs
the full path /api/v1/metrics, and the authorization secret should not
include a "Bearer" prefix. The form previously gave no guidance on
either — same generic placeholder as HTTP monitors and no helper text.

- Replace the URL field's generic label/placeholder with hardware-
  specific copy ("Capture endpoint", realistic 192.168.1.10:5001/...
  example) and add helper text under the field.
- Add helper text under the authorization secret field clarifying that
  only the raw token belongs there — Bearer is added automatically.
- Wire optional urlHelperText / secretHelperText through
  GeneralSettingsConfig so other monitor types are unaffected.
- Add three new i18n keys (captureUrl.label/placeholder/helperText)
  and one (secret.helperText) to all 16 locales. Non-English locales
  carry the English copy as a placeholder until translators pick the
  keys up via the existing PoEditor pipeline.
2026-05-01 16:46:46 -04:00
Alexander Holliday 5b25666054 Merge pull request #3627 from bluewave-labs/feat/generate-api-docs-from-schema
feat: Generate API docs from zod schema
2026-05-01 12:20:25 -07:00
Alex Holliday 34feb6d3fa replace inline objects with defined constants 2026-05-01 11:31:52 -07:00
Alex Holliday a57da284d7 remove unused import 2026-05-01 11:31:52 -07:00
Alex Hollidayandgorkem-bwl 3f9019f34c move openapi out of src dir
Adapted from #3579 by @gorkem-bwl. The openapi/registry, helpers, and
per-resource routes/* files are largely their work; this commit relocates
the directory out of src/ so the build-time generator code is not emitted
into dist/, and shifts spec generation from runtime to a build step.

Co-authored-by: gorkem-bwl <gorkem.cetin@bluewavelabs.ca>
2026-05-01 11:31:48 -07:00
Retr01-sudo 4e284c1468 feat(monitors): integrate bulk actions into uptime and infrastructure tables
- Mounts selection checkboxes in UptimeMonitorsTable and MonitorsTable
- Enhances global Table.tsx to cleanly render checkboxes on mobile cards via mobileLabel
- Injects BulkActionsBar into main list views

Fixes #3602
2026-05-01 19:44:07 +05:30
Retr01-sudo 6b9759e455 feat(monitors): create bulk actions ui components and selection hooks
- Extracts generic table selection logic into useTableSelection hook
- Creates BulkActionsBar component with Collapse animations
- Implements useBulkMonitorActions to handle API logic and toasts
- Adds i18n translation strings across all 16 locales

Fixes #3602
2026-05-01 19:43:27 +05:30
Retr01-sudo 094def117a feat(monitors): add bulk pause/resume api endpoints and service logic
- Validates bulk monitor IDs using zod
- Implements optimized Mongo $in/$ne queries for performance
- Orchestrates background job pausing/resuming via BullMQ
- Added comprehensive unit tests for monitor service logic

Fixes #3602
2026-05-01 19:42:51 +05:30
Gorkem Cetin 28baeaf218 Remove OpenSSF Best Practices badge
Removed OpenSSF Best Practices badge from README.
2026-04-30 20:14:25 -04:00
Alexander Holliday c535ba8c1a Merge pull request #3625 from bluewave-labs/fix/status-page-theme-refactor
fix: extract duplicate code, add slot components
2026-04-30 14:47:40 -07:00
Alex 4c04294f71 extract duplicate code, add slot components 2026-04-30 14:42:19 -07:00
Alexander Holliday 212c1fec52 Merge pull request #3623 from bluewave-labs/feat/raise-max-recent-checks
feat: raise max recent checks
2026-04-30 14:12:44 -07:00
Alex Holliday 05798914a8 Merge branch 'develop' into feat/raise-max-recent-checks 2026-04-30 11:01:27 -07:00
Alexander Holliday 6a4d8a3f6b Merge pull request #3624 from bluewave-labs/fix/status-page-mutate-on-save
fix: mutate status page on save
2026-04-30 11:01:01 -07:00
Alex Holliday e906b237a0 mutate status page on save 2026-04-30 10:57:01 -07:00
Alex Holliday bacbf8404e refactor tests 2026-04-30 10:36:46 -07:00
Alex Holliday d47cc65fed Increase max recent checks limit from 25 -> 50, extract constants 2026-04-30 10:30:54 -07:00
Alexander Holliday e077fe52e7 Merge commit from fork
Remove Docker-Compose Port mapping on dist-arm
2026-04-30 09:59:12 -07:00
Alexander Holliday f39029a37f Merge pull request #3622 from bluewave-labs/chore/bump-fast-xml-parser
chore(deps): bump fast-xml-parser (transitive)
2026-04-30 09:42:01 -07:00
Alexander Holliday c8c54ce49e Merge pull request #3621 from bluewave-labs/chore/bump-protocol-buffers-schema
chore(deps): bump protocol-buffers-schema (transitive)
2026-04-30 09:41:42 -07:00
Alexander Holliday 021eda098d Merge pull request #3620 from bluewave-labs/chore/bump-yaml
chore(deps): bump yaml (transitive)
2026-04-30 09:41:27 -07:00
Alexander Holliday 8993ecadce Merge pull request #3619 from bluewave-labs/chore/bump-postcss-server
chore(deps): bump postcss (transitive)
2026-04-30 09:41:13 -07:00
Alexander Holliday f92e0fae3a Merge pull request #3618 from bluewave-labs/chore/bump-postcss-client
chore(deps): bump postcss (transitive)
2026-04-30 09:40:55 -07:00
Alexander Holliday a4b73bc4ed Merge pull request #3617 from bluewave-labs/chore/bump-follow-redirects
chore(deps): bump follow-redirects (transitive)
2026-04-30 09:40:38 -07:00
Alexander Holliday 87b7ff7452 Merge pull request #3616 from bluewave-labs/chore/bump-vite
chore(deps): bump vite to ^6.4.2
2026-04-30 09:40:22 -07:00
Alexander Holliday 61de273a73 Merge pull request #3615 from bluewave-labs/chore/bump-flatted
chore(deps): bump flatted (transitive)
2026-04-30 09:40:00 -07:00
Alexander Holliday 04332c47d5 Merge pull request #3614 from bluewave-labs/chore/bump-picomatch
chore(deps): bump picomatch (transitive)
2026-04-30 09:39:45 -07:00
Alexander Holliday f263c78a4d Merge pull request #3613 from bluewave-labs/chore/bump-lodash
chore(deps): bump lodash to 4.18.1 (transitive)
2026-04-30 09:39:24 -07:00
Alexander Holliday de67ea9ee6 Merge pull request #3612 from bluewave-labs/chore/bump-multer-lts
chore(deps): bump multer to 1.4.5-lts.2
2026-04-30 09:39:04 -07:00
Alexander Holliday c9c6c42116 Merge pull request #3611 from bluewave-labs/chore/bump-axios
chore(deps): bump axios to 1.15.2
2026-04-30 09:38:41 -07:00
gorkem-bwl 79d2eb68fa chore(deps): bump vite to ^6.4.2
Patches CVE-2026-39365 (medium — path traversal in optimized deps .map handling).
Relaxes pinned version 6.4.1 to ^6.4.2 patch range; semver-compatible.
2026-04-30 01:48:16 -04:00
gorkem-bwl bc9441bcd0 chore(deps): bump fast-xml-parser (transitive)
Patches CVE-2026-41650 (medium — XML comment and CDATA injection via unescaped delimiters in XMLBuilder).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:47:42 -04:00
gorkem-bwl d868f6931d chore(deps): bump protocol-buffers-schema (transitive)
Patches CVE-2026-5758 (medium — prototype pollution in protocol-buffers-schema).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:47:34 -04:00
gorkem-bwl 29dbebd228 chore(deps): bump yaml (transitive)
Patches CVE-2026-33532 (medium — stack overflow via deeply nested YAML collections).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:47:08 -04:00
gorkem-bwl 906d306827 chore(deps): bump postcss (transitive)
Patches CVE-2026-41305 (medium — XSS via unescaped </style> in CSS stringify output).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:46:46 -04:00
gorkem-bwl aaf7d7e47d chore(deps): bump postcss (transitive)
Patches CVE-2026-41305 (medium — XSS via unescaped </style> in CSS stringify output).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:46:28 -04:00
gorkem-bwl c40f4e9f1c chore(deps): bump follow-redirects (transitive)
Patches GHSA-r4q5-vmmm-2653 (medium — leaks Custom-Authorization headers to cross-domain redirect targets).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:46:07 -04:00
gorkem-bwl 35daed605f chore(deps): bump flatted (transitive)
Patches CVE-2026-33228 (high — prototype pollution via parse() in flatted).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:45:33 -04:00
gorkem-bwl 6434afe2c9 chore(deps): bump picomatch (transitive)
Patches CVE-2026-33671 (high — ReDoS via extglob quantifiers) and CVE-2026-33672 (medium — method injection via POSIX character classes).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:44:39 -04:00
gorkem-bwl 788ca78234 chore(deps): bump lodash to 4.18.1 (transitive)
Patches CVE-2026-4800 (high — code injection via _.template) and
CVE-2026-2950 (medium — prototype pollution via array path bypass).

Transitive bump within existing semver range; no package.json change.
2026-04-30 01:44:00 -04:00
gorkem-bwl 3adadf0d07 chore(deps): bump multer to 1.4.5-lts.2 in server
## Changes
- Pin multer from ^1.4.5-lts.1 to ^1.4.5-lts.2

## Benefits
- Picks up the latest 1.x LTS patch (released 2025-03-20)
- Patch-level bump within existing semver range; no API changes
- Two routes use multer (authRoute, statusPageRoute) and both rely only on the stable basic API
2026-04-30 01:27:14 -04:00
gorkem-bwl 55b9702fc3 chore(deps): bump axios to 1.15.2 in client and server
## Changes
- Bump axios from ^1.7.x to ^1.15.2 in both client/ and server/

## Benefits
- Patches CVE-2025-62718 (NO_PROXY hostname normalization SSRF)
- Patches CVE-2026-40175 (cloud metadata exfiltration via header injection)
- Minor bump within existing ^1.x semver range; no API changes
2026-04-30 01:26:14 -04:00
Gorkem Cetin 7f4282ab1f Update README.md 2026-04-29 17:46:22 -04:00
Gorkem Cetin fcf2557a2f Update README to remove sponsorship and refine features
Removed sponsorship links and consolidated monitoring options.
2026-04-29 17:04:01 -04:00
Alexander Holliday bdf1e13ce8 Merge pull request #3607 from bluewave-labs/feat/smtp-from-name
feat(smtp): add optional sender display name to SMTP configuration
2026-04-29 13:01:08 -07:00
Alex Holliday 4de2ba4b25 add missing tests 2026-04-29 12:59:48 -07:00
Alex Holliday d5bf45e2a9 allow from 2026-04-29 12:57:37 -07:00
gorkem-bwl 6f988cc688 fix(settings): drop from field from transport JSON preview
The transport JSON preview is strictly for verifying what is handed to
nodemailer.createTransport. The From header is built per-message in
sendMail, so showing it inside the transport JSON was misleading. The
separate From: ... pre block below the JSON is the right place for
that preview.
2026-04-29 15:48:12 -04:00
gorkem-bwl 8fbbb1dd4b Revert "refactor(email): set From at transport level instead of per-message"
This reverts commit baa4d693ae.
2026-04-29 15:42:02 -04:00
gorkem-bwl baa4d693ae refactor(email): set From at transport level instead of per-message
The /settings transport preview shows `from` inside the createTransport
JSON, but the implementation was passing it per-message via sendMail.
Switch to passing it as part of the transport config so what the
preview shows matches what is actually built — no behavioural change
for outgoing emails since nodemailer treats transport-level `from` as
the default for every sendMail call.
2026-04-29 15:38:22 -04:00
gorkem-bwl e11553c218 feat(settings): restore From-header preview block under SMTP transport JSON
Brings back the second pre-block I dropped when consolidating the
preview. The transport JSON shows what's handed to nodemailer
createTransport; the From-header block shows the rendered envelope
recipients will see — they answer different questions and both belong.

Conditioned on systemEmailAddress so empty configs don't render an
empty pre.
2026-04-29 15:33:15 -04:00
gorkem-bwl e71892d0bc feat(smtp): add optional sender display name to SMTP configuration
Closes #3600. Previously the From header on test emails and notifications
showed only the raw email address. Adds an optional "Sender name" field
so emails appear from a friendly name like "Checkmate notifications"
<user@example.org>.

## Behaviour
- Field is optional. When empty/null/whitespace, the From header falls
  back to the raw email address — identical to current behaviour, no
  regression for existing installs.
- When set, the email service hands nodemailer { name, address } as the
  message from field. Nodemailer handles RFC-5322 encoding (quotes,
  commas, non-ASCII) so the display name is safe for any input.
- Unsetting works: clearing the input transforms the value to null in
  the Zod parse step, and the settings repository already turns null
  values into a $unset operation.
- Both client and server Zod schemas trim and empty-string-to-null,
  preventing whitespace-only display names from being stored.

## Surfaces
- AppSettings schema, Settings type, EmailTransportConfig pick, server
  Zod validation, client Zod validation, client form defaults, and the
  Settings page form all carry the new systemEmailDisplayName field.
- The transport preview pre-block on /settings now includes a `from`
  key showing exactly what the email service hands nodemailer — the
  string when no display name is set, or { name, address } when it is.
- en.json carries the new label and placeholder. Other locales fall
  back until translated (16 locales pending — separate follow-up).

## Conventions conformance (per docs/frontend-conventions.md)
- The pre-block I touched had three rule #1 violations (sx p,
  borderRadius, backgroundColor); hoisted those to native props using
  theme.shape.borderRadius. Other rule #1 violations elsewhere on this
  page are pre-existing and intentionally left alone.
- No new sx-with-native-equivalents introduced.
- No color="text.X" shorthand introduced.
- No theme module import introduced.
2026-04-29 15:30:16 -04:00
Alexander Holliday 100e569aa8 Merge pull request #3583 from ayshaL/fix/blank-screen-on-timezone
Fix: prevent blank screen crash when clearing timezone search field
2026-04-29 12:13:40 -07:00
Alexander Holliday 343167012e Merge pull request #3605 from bluewave-labs/fix/reveresed-status-page-checks
Fix/reveresed status page checks
2026-04-29 12:06:45 -07:00
Alex Holliday 5330fc3183 bump version 2026-04-29 12:04:44 -07:00
Alex Holliday df7e348366 Merge branch 'develop' into fix/reveresed-status-page-checks 2026-04-29 12:04:22 -07:00
Alex Holliday b2c3e08db3 fix reveresed checks 2026-04-29 12:03:23 -07:00
Alexander Holliday 8ca62db317 Merge pull request #3603 from bluewave-labs/chore/bump-version
bump version
2026-04-29 11:51:00 -07:00
Alex Holliday 3bb767b780 bump version 2026-04-29 11:50:28 -07:00
Alexander Holliday 20d27cb984 Merge pull request #3568 from bluewave-labs/feat/status-page-themes-backend
feat(status-page): add theme and themeMode fields (backend)
2026-04-29 11:45:50 -07:00
Alex Holliday 0322a96090 update settings service tests 2026-04-29 11:41:22 -07:00
Alex Holliday 3dbad1743a resolve conflicts 2026-04-29 11:39:08 -07:00
Alex Holliday f4c3ee16db Merge branch 'develop' into feat/status-page-themes-backend 2026-04-29 11:35:55 -07:00
Alexander Holliday a7d5a84206 Merge pull request #3569 from bluewave-labs/feat/status-page-themes-frontend
feat(status-page): four selectable themes for the public status page
2026-04-29 11:35:23 -07:00
Alex Holliday e86734cee9 resolve conflcits 2026-04-29 11:28:57 -07:00
Alex Holliday cc84c37a2d remove planning docs 2026-04-29 11:22:20 -07:00
Alex Holliday e85e9dc2d4 remove duplicated time util 2026-04-29 11:15:33 -07:00
Alex Holliday 1398ab6721 remove dead code 2026-04-29 11:12:35 -07:00
Alex Holliday d32ea7f8c3 remove dead code 2026-04-29 11:07:34 -07:00
Alex Holliday edeed00be6 fix unnecessary cast, remove dead keys 2026-04-29 10:56:57 -07:00
Gorkem Cetin 4c7e96122f Merge pull request #3599 from bluewave-labs/feat/frontend-conventions-eslint
docs(client): document frontend conventions and enforce via ESLint
2026-04-29 13:56:23 -04:00
Alex Holliday 45c02e0f7f unsafe lookup fix 2026-04-29 10:51:40 -07:00
gorkem-bwl aa7051095a docs(client): document frontend conventions and enforce via ESLint
Capture the five recurring code-review patterns from the recent PR series
(#3591–#3596) as enforceable rules so they don't have to be raised again
in review.

## Conventions
- Prefer MUI native props over `sx` (e.g. color={...}, mt={...}, width={...}).
- Use the full theme path for colors: theme.palette.text.secondary, never
  the "text.secondary" shorthand string. The shorthand is invisible to
  grep, which makes future audits and refactors miss callsites.
- No hardcoded numeric or string literals — use LAYOUT, typographyLevels,
  theme.shape.borderRadius, theme.palette tokens.
- Use useTheme() inside components; don't import { theme } from
  @/Utils/Theme/Theme (factory output, not a live mode-aware instance).
- Pair runtime tuples with derived types so they can't drift.

## Files
- docs/frontend-conventions.md — long-form doc with before/after examples
  drawn from the actual review fixes, plus a pre-PR checklist.
- CLAUDE.md — short summary in the Code Conventions section pointing to
  the doc, so it's loaded into agent context at session start.

## ESLint enforcement
- client/.eslintrc.cjs — four no-restricted-syntax selectors for each rule:
  - color="…" / bgcolor="…" / borderColor="…" with a dotted shorthand value
  - sx={{ key: ... }} where key has a native equivalent, only on hoistable
    components (Box, Stack, Typography, Grid, Container, Paper, Card*,
    Divider, Link, Chip) — Dialog/Drawer/Menu etc. only accept sx.
  - import { theme } from "@/Utils/Theme/Theme"
  - Typography fontSize={13} (the body default — drop the prop).
- TypeScript files are now actually parsed: added @typescript-eslint/parser
  and @typescript-eslint/eslint-plugin and a TS override that extends the
  React plugin configs. Several @typescript-eslint/recommended rules
  disabled to avoid noise on the existing codebase.
- client/package.json — lint script now covers .ts/.tsx; new lint-strict
  script for CI use once the codebase is conformant; --max-warnings 0
  removed from `lint` so existing-code warnings don't block the workflow.

## Surface
Running `npm run lint` after this change reports ~280 warnings against
the existing codebase, almost all from rule #1 (sx-vs-native). These are
intentionally warnings, not errors — they surface the work without
blocking. Each warning links to the rule number in the conventions doc
so the reasoning is one click away.
2026-04-29 13:49:58 -04:00
Alex Holliday fcb4fbf277 fix optional types 2026-04-29 10:35:35 -07:00
Alex Holliday 74a12754b6 fix controller architecture 2026-04-29 10:33:46 -07:00
Alexander Holliday fe8213f8bb Merge pull request #3596 from bluewave-labs/feat/settings-remove-monitors-modal
feat(settings): explanatory body in Remove monitors confirmation modal
2026-04-29 10:03:15 -07:00
Alex Holliday cf5691e250 fix build error 2026-04-29 10:01:43 -07:00
Alex Holliday f3b4b908fd Merge branch 'develop' into feat/settings-remove-monitors-modal 2026-04-29 10:00:01 -07:00
Alexander Holliday f58456961b Merge pull request #3595 from bluewave-labs/feat/infrastructure-network-empty-state
feat(infrastructure): show notice when network charts have zero traffic
2026-04-29 09:57:49 -07:00
Alex Holliday 91bdc662ca Merge branch 'develop' into feat/infrastructure-network-empty-state 2026-04-29 09:56:55 -07:00
Alexander Holliday 53e04e6af2 Merge pull request #3594 from bluewave-labs/feat/incidents-page-polish
feat(incidents): consistent typography, icons, and layout across the page
2026-04-29 09:56:27 -07:00
Alex Holliday 301942d0eb normalize 2026-04-29 09:56:01 -07:00
Alex Holliday 29bb1615f5 Merge branch 'develop' into feat/incidents-page-polish 2026-04-29 09:51:06 -07:00
Alexander Holliday afc3ad237f Merge pull request #3593 from bluewave-labs/feat/account-team-modals
feat(account): side-by-side name and password fields in Add Member modal
2026-04-29 09:50:34 -07:00
Alex Holliday e0ac040063 Merge branch 'develop' into feat/account-team-modals 2026-04-29 09:49:23 -07:00
Alexander Holliday f895125145 Merge pull request #3592 from bluewave-labs/refactor/button-input-styling
refactor(inputs): softer button hover, sx-forwarding TextInput, themed FieldLabel
2026-04-29 09:47:31 -07:00
Alex Holliday 63031e938e normalize 2026-04-29 09:47:11 -07:00
Alex Holliday 4b82129199 Merge branch 'develop' into refactor/button-input-styling 2026-04-29 09:46:54 -07:00
Alexander Holliday ccd176ba0b Merge pull request #3591 from bluewave-labs/refactor/dialog-component
refactor(dialog): standard modal layout, NoticeBanner design-element, softer Cancel
2026-04-29 09:43:12 -07:00
Alex Holliday 2dd9218910 normailze dialog and basepage 2026-04-29 09:42:49 -07:00
Alex Holliday 5792ca624e normalize NoticeBanner 2026-04-29 09:37:37 -07:00
gorkem-bwl 5716b5eea4 feat(settings): explanatory body in Remove monitors confirmation modal
The remove-all-monitors confirmation dialog only had a one-line title
and an empty body, which looked awkward and didn't communicate what was
actually about to be deleted alongside the monitors themselves.

- Adds a paragraph explaining that check history, incidents, scheduled
  maintenance windows, and dependent status pages will also be removed,
  with an inline tip to export monitors first to keep a backup.
- The Confirm button now uses confirmColor='error' to make the
  destructive action visually distinct, with a localized
  'Remove monitors' label.
- Translated to all 16 locales, with terminology aligned to each
  locale's existing usage (e.g. zh-CN/zh-TW use 监控/監控 to match the
  existing button label, vi uses 'trình giám sát', etc.).
2026-04-28 23:58:13 -04:00
gorkem-bwl c89dbebe98 feat(infrastructure): show notice when network charts have zero traffic
When Capture runs inside a container (typical for Docker Desktop), the
only visible interfaces are loopback, so every bytes_sent/bytes_recv
counter is zero and the charts render as flat lines without any
explanation. Users couldn't tell whether Capture was unreachable, the
monitor was broken, or there was just genuinely no traffic.

Detect the case where checks exist (chart configs were built) but every
interface across every check reports zero bytesSentPerSecond and
deltaBytesRecv, and render a NoticeBanner above the chart grid
explaining it and suggesting Capture be run on the host.

Adds pages.infrastructure.charts.labels.netNoTraffic to all 16 locales.
2026-04-28 23:57:14 -04:00
gorkem-bwl 5d5d1dbb23 feat(incidents): consistent typography, icons, and layout across the page
- All uppercase headers (Active Incidents, Latest Incidents, Incident
  Statistics, Resolved Incidents, summary card titles, details card
  sections) now use the existing 'eyebrow' typography variant. That's
  the same style used on the Logs page and lives in Theme.ts so future
  tweaks land in one place.
- Latest Incidents and Incident Statistics rows now share a minHeight
  so both cards line up vertically; Latest Incidents items dropped
  their extra dividers and Box wrappers.
- Body text in the summary cards (and the details dialog) uses
  variant=body1 (13px) consistently — previously a mix of body1/body2
  and unspecified Typography.
- All lucide icons in CardSummary go through the design-system <Icon>
  wrapper so they share the project's strokeWidth=1.5 default rather
  than lucide's default 2.
- ControlsIncidentFilter: replaced the missing
  pages.incidents.filters.clearFilters key (which was rendering as
  raw text) with the existing common.buttons.clearFilters.
- CardDetails: extracted local SectionHeading and Cell helpers; switched
  the cards' bare <Typography> cells to variant=body1 + variant=eyebrow
  for headings; dropped redundant title-dividers (eyebrow style already
  separates visually).
2026-04-28 23:56:10 -04:00
gorkem-bwl 93505f006c feat(account): side-by-side name and password fields in Add Member modal
- AddTeamMemberDialog: First name / Last name now share a row, and
  Password / Confirm password share a row. Each field uses sx={flex:1}
  so the row totals match the width of the email/role rows below.
- InviteTeamMemberDialog: drop the redundant mt={theme.spacing(...)}
  workaround on the form Stack (the new Dialog component provides this
  spacing).
- Constants conformance: spacing literals replaced with LAYOUT.XS.
2026-04-28 23:55:27 -04:00
gorkem-bwl a6204b7bf5 Merge branch 'refactor/button-input-styling' into feat/account-team-modals 2026-04-28 23:54:48 -04:00
gorkem-bwl 9c21d71c44 refactor(inputs): softer button hover, sx-forwarding TextInput, themed FieldLabel
- Button.tsx: contained variant's hover now uses HOVER.DARKEN=0.06
  instead of MUI's default 16%, which was visibly heavy on the green
  primary buttons across the app. Also fixes a subtle bug where the
  earlier hover sx merge order silently dropped the boxShadow:'none'
  reset. Added a HOVER token group to constants.ts so the darken value
  has a single source of truth.
- TextInput.tsx: forward the caller's sx to the wrapper Stack when a
  fieldLabel is present (previously it was applied to the inner
  TextField, so flex:1 etc. didn't propagate to the visible width).
  Inner sx now also explicitly width:100% so wrapped inputs don't
  collapse to content width when their parent is a flex row.
- FieldLabel.tsx: drop the hardcoded fontSize:'14px' and use
  variant=body1 (=13px from theme) to match the rest of the app's
  body typography.
2026-04-28 23:54:39 -04:00
gorkem-bwl fef3f2c294 refactor(dialog): standard modal layout, NoticeBanner design-element, softer Cancel
Dialog component:
- Title bar gets a subtle background tint + bottom border. Title is
  variant=h2 weight=600. Description tucks under the title with
  variant=body1 secondary color (replaces MUI's DialogContentText).
- Action bar gets a matching subtle tint + top border. Cancel is now
  outlined (was contained-error, which made every modal's Cancel button
  look destructive). Confirm keeps its color (primary by default,
  caller can pass error for destructive flows).
- Content area sits between with its own breathing room baked in, so
  callers no longer need mt={theme.spacing(...)} workarounds.
- The cancelColor prop is removed; sites that genuinely need a
  destructive Cancel can use confirmColor='error' on the Confirm button
  instead.

NoticeBanner design-element:
- Extracted from the inline alert pattern in PageSpeedKeyPriorityFallback
  into a reusable component with severity=info|warning|error.
- Refactored PageSpeedKeyPriorityFallback to use it.

DialogResolution.tsx: drop the now-removed cancelColor prop and the
mt-Box wrapper that's no longer needed (Dialog provides the spacing).
2026-04-28 23:53:59 -04:00
Gorkem Cetin a617bd340a Merge pull request #3590 from bluewave-labs/feat/game-monitor-helper-link
feat(create-monitor): explain server-side query setup for game monitors
2026-04-28 18:05:41 -04:00
gorkem-bwl 23061e31d1 i18n(create-monitor): translate game description for the remaining 9 locales
Fills in machine translations for ar, cs, fi, fr, ja, pt-BR, tr, uk, vi
so every locale now ships a native translation of the game-monitor
description (including the new server-query setup sentence and the
<gamedigLink> placeholder).
2026-04-28 18:02:57 -04:00
gorkem-bwl 34b1a9cdc2 feat(create-monitor): explain server-side query setup for game monitors
The game monitor type relies on each game's native query protocol — many
require a config file change on the game server (e.g. Minecraft Java
needs `enable-query=true` in `server.properties`). Without that, the
monitor silently fails to reach the server.

Append a sentence to the game-type general settings description that:

- tells the user some games need server-side queries enabled
- points them to the GameDig supported-games list for protocol details

Implementation:

- Use <Trans> with a `gamedigLink` slot so the link renders inline
- Loosen ConfigBox `subtitle` from `string` to `React.ReactNode` so the
  subtitle can host the <Trans> output
- Update all 16 locale files. Six (de, es, ru, th, zh-CN, zh-TW) get a
  full translation of the new sentence; nine empty ones (ar, cs, fi, fr,
  ja, pt-BR, tr, uk, vi) fall back to the English string per the locale
  gap policy
2026-04-28 17:55:24 -04:00
gorkem-bwl bab65770ce refactor(status-page): simplify renderer lookup, extract route prefix, memoize wrapper style
Four small follow-ups from a code review pass:

1. Drop the redundant `?? THEMED_RENDERERS[DEFAULT_STATUS_PAGE_THEME]`
   fallback in Status/index.tsx — the map is now a total Record, so the
   first lookup cannot return undefined.
2. Extract the literal "/status/public" into PUBLIC_STATUS_PAGE_PREFIX in
   Types/StatusPage.ts and reuse from the three runtime call sites
   (Status/index.tsx, HeaderStatusPageControls.tsx, StatusPagesTable.tsx).
   The Routes/index.tsx route definition keeps its string literal — the
   route table is otherwise all literals and the prefix only matters when
   it's interpolated.
3. Delete two narrating comments above the public/admin branches in
   Status/index.tsx — the JSX is self-describing.
4. Move the inline `style` object construction in StatusPageThemeProvider
   into the existing useMemo so the wrapper div receives a stable style
   reference. The previous spread defeated the surrounding memoization on
   every render.
2026-04-28 16:52:16 -04:00
gorkem-bwl ed3645c3d0 refactor(status-page): drop legacy single-design renderer and CSS overrides
The 4 themed renderers (refined, modern, bold, editorial) cover every
StatusPageTheme value and the model defaults to "refined", so the legacy
fallback path in Status/index.tsx was unreachable in practice.

Removing it lets us delete:

- theme-overrides.css (the only remaining CSS file in StatusPage)
- StatusBar / MonitorsList / InfrastructureMetrics (only the legacy
  fallback consumed them)
- The .sp-* className hooks that wired CSS rules into the components

Pure sx via the per-theme styles.ts recipes is now the single source of
truth for every status page surface. Net diff is -801 / +42.

Status/index.tsx now resolves a renderer from the theme map (falling back
to the default theme) and keeps the public-route and admin-preview
branches it already had.
2026-04-28 16:38:35 -04:00
Alexander Holliday 0654e4adfa Merge pull request #3589 from bluewave-labs/feat/alert-on-initial-down
feat: incident on initializing && down
2026-04-28 10:14:54 -07:00
Alex Holliday 54c0e089a8 incident on initializing && down 2026-04-28 10:09:42 -07:00
Gorkem Cetin aee81a03fa Merge pull request #3588 from bluewave-labs/docs/contributing-redis-formatcheck
docs: align CONTRIBUTING.md and CLAUDE.md with current toolchain
2026-04-28 00:00:25 -04:00
gorkem-bwl 40bcf7f6ac docs: align CONTRIBUTING.md and CLAUDE.md with current toolchain
CONTRIBUTING.md: surface format-check + build in code guidelines
and the PR checklist so contributors run the same verifications
the project enforces locally.

CLAUDE.md: correct stale test framework reference (Mocha+Chai+Sinon
-> Jest) and update the test file glob to server/test/**/*.test.ts.
2026-04-27 23:50:01 -04:00
Gorkem Cetin e908d34b3a Remove sponsors section from README
Removed sponsorship section from README.
2026-04-27 23:39:22 -04:00
Gorkem Cetin a706e68c6a Remove documentation and translation improvement sections
Removed sections about improving documentation and translations from the contributing guidelines.
2026-04-27 23:38:37 -04:00
gorkem-bwl a2786a77ec refactor(status-page): tighten the theme-fields backend after second review
Apply review feedback:
- Extract makeStatusPage from the two test files that duplicated it (test/unit/services/statusPageService.test.ts and test/unit/controllers/statusPageThemeFlag.test.ts) into test/helpers/makeStatusPage.ts. Both files now import the shared helper, matching the existing test/helpers/ convention used for repository fakes and provider contracts
- Add a blank line in statusPageController.getStatusPageByUrl between the normalizedMonitors map and the themesEnabled lookup so the response-shaping section breathes a bit
- Drop the obsolete localStorage references from the plan doc that survived from an earlier draft of the design — the public page never persisted a per-visitor mode choice in localStorage, so the test bullet under 2.11 and the risk row both pointed at functionality that does not exist
2026-04-27 19:33:03 -04:00
gorkem-bwl a5af4e48df Merge remote-tracking branch 'origin/develop' into feat/status-page-themes-backend 2026-04-27 19:29:44 -04:00
gorkem-bwl d80d939bd8 test(status-page): cover theme-flag helpers and document remaining caveats
Address review follow-ups on PR #3568:

- Extract the controller's withoutThemeFields and applyDefaultTheme helpers from a private class field into a standalone module (controllers/statusPageThemeFlag.ts) so they can be tested without a controller harness; the controller is now a one-line consumer
- Add 9 unit tests covering both helpers (test/unit/controllers/statusPageThemeFlag.test.ts): no-mutation guarantees, partial inputs, all-fields-preserved verbatim, override-with-defaults semantics, and "page has no theme set yet" — the flag-off path is now exercised in both directions
- Expand the comment over withoutThemeFields to spell out why the strip is silent rather than a 400: this is an ops kill switch, not API semantics; clients shouldn't need to know whether themes are disabled today

Two caveats called out in review that this commit does NOT change:
- Mongoose `default:` directives only inject during hydration, not on partial updates. Legacy documents updated with `findOneAndUpdate` still won't have `theme` / `themeMode` written to Mongo. They render as defaults on read because the schema injects them — functionally equivalent, but the DB will hold a mix of present and absent fields indefinitely. Acceptable per the original commit intent.
- docs/plans/status-page-themes.md (304 lines) covers both backend and frontend work for the themes feature. It lives in this PR as the design artefact that drove both branches; moving it for purity would break the "where did this come from?" trail. Leaving it in.
2026-04-27 19:26:16 -04:00
gorkem-bwl 5874efff3c refactor(status-page): apply review feedback on theme-fields backend
- StatusPage Mongoose model: import DEFAULT_STATUS_PAGE_THEME / DEFAULT_STATUS_PAGE_THEME_MODE from types/statusPage so the four sources of the default literal stay locked together
- openapi.json: add explicit "default" alongside the enum on StatusPageTheme and StatusPageThemeMode component schemas; documentation already mentioned the defaults but the JSON didn't carry them
- statusPageController: add JSDoc-style comments above withoutThemeFields and applyDefaultTheme spelling out the asymmetric strip-on-write / force-on-read semantics so the next reviewer doesn't have to re-derive the design
2026-04-27 19:02:44 -04:00
gorkem-bwl ab9e722caa refactor(status-page): consolidate font stacks and pill recipes
Second /simplify pass picked up:
- pillHardware in refined and modern hardcoded blue hex codes (#60a5fa, #1d4ed8, #2563eb) while bold and editorial already used the green token system. Switch refined and modern to tokens.up / tokens.upSoft so all four themes are consistent and the public status page no longer leaks blue
- pill and pillHardware in refined and modern duplicated the shared 6 typography fields (fontSize, textTransform, letterSpacing, padding, borderRadius, fontWeight). Extract a pillBase constant per theme and spread, leaving only the colour deltas on each
- The 4 themes each defined their own font-stack constants. Consolidate them in themes/shared/fontStacks.ts (SANS_STACK, BOLD_SANS_STACK, SERIF_STACK, EDITORIAL_SECONDARY_SANS_STACK, MONO_STACK) and import. Drops 6 duplicated literal stacks
- modern dark-mode card hover used the light-mode shadow as a fallback. Add darkShadowHover and pick it up via cardHoverShadow when isDark
- refined and editorial styles factories silence their unused isDark parameter with `void isDark` instead of an underscore prefix; bold and modern still use it actively
2026-04-27 18:52:53 -04:00
gorkem-bwl 4f8f2f1422 refactor(status-page): tighten the bold/modern/editorial conversion
Apply review feedback:
- Editorial chartSwitch: drop the per-button isFirst flag and put the divider on the parent via "& > button + button { borderLeft }"; chartSwitchButton(active) now matches bold/modern's signature
- Drop the empty company: {} entries in bold/modern and the wrapper span around the company name; the parent brand row already styles it
- Editorial styles factory: keep the (tokens, isDark) signature for cross-theme uniformity but acknowledge the unused isDark with a void-statement instead of an underscore prefix
2026-04-27 18:45:45 -04:00
gorkem-bwl c6c50063d8 refactor(status-page): convert bold, modern and editorial themes to MUI sx
Finishes the migration started for refined. Each theme now consumes the same pattern:

- A typed *Styles interface that names every entry the page consumes (no `as Function` casts at the call site)
- A *Styles factory that takes (tokens, isDark) and returns sx recipes derived from existing tokens.ts plus the shared toneColor/toneSoft helpers in shared/overallStatus
- A useMemo around the factory call so MUI's sx processor sees referential equality across renders that don't change tokens
- modern.css's keyframes (pulse + staggered fade-in + reduced-motion guard) ported via @mui/system keyframes inside the modern styles factory
- bold.css's hero glow + conic logo + pseudo-elements ported as inline sx via "&::before" / "& > *" selectors
- editorial.css's serif typography + hairline dividers + flat rectangular badges ported as plain sx with the serif/sans family stacks named at module scope

Drop the four .css files (~1789 lines) along with the className= attributes and the import "./*.css" lines in each StatusPage component.

The shared themed components (ThemedHeatmap, ThemedHistogram, ThemedInfrastructure) lose their legacy classPrefix branch entirely now that no theme uses it. The dual-API gate is gone; there is one path.

Verified: tsc --noEmit clean, prettier clean.

Note: theme-overrides.css remains for the legacy non-themed status page components (StatusBar, MonitorsList, HeaderStatusPageControls). That's separate scope from the four-theme work the reviewer flagged; can be migrated in a follow-up.
2026-04-27 18:39:42 -04:00
gorkem-bwl b95d8886b9 refactor(status-page): tighten the refined-theme conversion
Apply review feedback:
- Type the styles factory's return as a named RefinedStyles interface so the consumer no longer needs `as Function` / `as any` casts on factory entries
- Lift toneColor / toneSoft to shared/overallStatus alongside the existing OverallTone type — the next three themes will need the same mapping
- Reuse heatLevel() in the legacy classPrefix branch of ThemedInfrastructure so both API paths share one threshold table
- Memoize the styles map in RefinedStatusPage so MUI's sx processor sees referential equality across renders that don't actually change tokens
- Make the dual-API gate in ThemedHeatmap / ThemedHistogram require ALL relevant sx props together so a partial migration can't silently fall back to the legacy class path
2026-04-27 18:30:04 -04:00
gorkem-bwl 163f5ec1ec refactor(status-page): convert refined theme to MUI sx + theme tokens
The reviewer's feedback on this PR is that the themes ship hardcoded class names against per-theme CSS files instead of following the project's MUI sx + theme.spacing()/palette pattern. This commit handles the refined theme as a proof of concept; the other three follow.

What changed
- New src/Pages/StatusPage/Status/themes/refined/styles.ts: a single function (tokens, isDark) -> sx recipes that mirror every rule in the deleted refined.css. All values are derived from the tokens already declared in tokens.ts so the colour/spacing source of truth doesn't change.
- RefinedStatusPage.tsx: drops the className= attributes and the import "./refined.css" in favour of sx={styles.x} drawn from the recipe map. Hardcoded class strings gone.
- refined.css: deleted (was 420 lines).
- Shared themed components (ThemedHeatmap, ThemedHistogram, ThemedInfrastructure): grew an opt-in sx-based API alongside the existing classPrefix one. The new API is what refined uses; bold / modern / editorial keep working unchanged on the legacy classPrefix path until they are migrated, after which the legacy code path is removed.

Verified
- tsc --noEmit clean
- prettier clean
- Pre-existing vite build error on this branch (flag-icons resolve, unrelated) is untouched

Next
- Convert the bold, modern and editorial themes the same way and remove the legacy classPrefix branch from the shared components.
2026-04-27 18:21:31 -04:00
Gorkem Cetin 959e2ff7b8 Merge pull request #3585 from bluewave-labs/chore/locale-completeness
chore(i18n): close translation gaps across all 16 locales
2026-04-27 11:24:38 -04:00
gorkem-bwl 2f8765cf3e chore(i18n): close translation gaps across all 16 locales
- Seed the 29 keys that were missing in every non-English locale (Pushover/Twilio config, register setup notice, public status-page status-bar messages, "service is down")
- Fill core UI strings (sidebar menu, buttons, table headers, password rules, image upload, auth footer) where they were left as empty values in the original translations
- Bring Spanish to 100% net coverage with a hand pass over remaining empties and same-as-English entries

Coverage net of empty/missing keys:
- 6 locales now at 100% (de, es, ru, th, zh-CN, zh-TW)
- All locales above 94% (worst: ar/fi at 94.8%, was 85.1% / 87.9%)
- Zero missing keys in any locale (was 29 each)

Some keys are intentionally same-as-English (HTTP, gRPC, Pagespeed, Docker etc.). Long-form copy in createMonitor descriptions and a few infrastructure stat labels remain empty in the lower-coverage locales for native review.
2026-04-27 11:21:48 -04:00
Iriome 49fcab2ee3 fix: allow containers to run as non-root user (UID 1000)
- chown public/, dist/, and scripts/ to node:node after build
- add USER node directive before EXPOSE in dist-mono and dist-arm Dockerfiles
- update CMD to exec form in dist-mono for consistency

Fixes running the container with --user 1000 in environments that
don't allow containers to run as root. The sed -i in inject-vars.sh
requires write permission on ./public/assets/, which previously
failed with 'Permission denied' when running as non-root.

The node user (UID 1000) is built into the node:20-slim base image.
Existing root deployments are unaffected.
2026-04-27 14:40:51 +01:00
Aicha Lassoued 5a72a11ac2 (client): format code change 2026-04-26 18:40:04 +01:00
Aicha Lassoued 66418bf591 fix: prevent blank screen crash when clearing timezone search field 2026-04-26 18:25:53 +01:00
gorkem-bwl 7342385729 fix(client): unblock the auth page build
Drop unused HeaderAuthControls/SPACING imports and switch the form-side wrapper from Stack to Box so the polymorphic component prop type-checks.
2026-04-26 02:29:23 -04:00
gorkem-bwl 671911dc1d feat(client): elevate auth pages with a two-column brand layout
- BaseAuthPage now renders a left form column and a right brand panel with eyebrow / tagline / description copy and a chess-board pattern over the brand green gradient
- Move the language selector and theme toggle to the page's top-right inside the form column so they read clearly in light and dark
- Every auth page (login, register, recovery, set new password) inherits the new layout
- Add brandPanel translations across all 16 locales; phrase the tagline as "your servers" instead of "stack" so it translates cleanly
2026-04-26 02:26:29 -04:00
gorkem-bwl 2ec55c6469 fix(client): replace lingering hardcoded colours with theme tokens
- Default status-page colour now uses the brand green instead of a royal-blue hex
- PageSpeed API-key alert pulls from theme.palette.warning + alpha for both modes instead of hand-rolled amber values
- Settings JSON preview block uses action.hover instead of the hand-rolled #1e1e1e/#f5f5f5 pair
- Drop the redundant #E0E0E0 thumb override on SwitchComponent so the theme MuiSwitch override actually wins
2026-04-26 01:59:09 -04:00
gorkem-bwl d678b3a304 fix(client): lighten the upload icon in ImageUpload
Match the rest of the icon system: 20px, 1.5 stroke, currentColor with 0.7 opacity instead of the lucide default heavy 24/2 stroke.
2026-04-26 01:56:28 -04:00
gorkem-bwl bf3bd50624 feat(client): unify card titles to the eyebrow style across uptime details
- StatBox titles now use the eyebrow variant (matches /uptime summary cards)
- BaseChart drops its leading icon block and renders the title via eyebrow so all chart cards share one header style
- Inside the average-response gauge, the qualitative label uses eyebrow with the status colour and the metric uses h1; the gauge track is a neutral disabled-action background so the active arc reads in light and dark
2026-04-26 01:56:22 -04:00
gorkem-bwl a4f69daae7 fix: surface notification test failures cleanly instead of as 500s
Server-side, return 200 with success: false and a friendly message when sendTestNotification reports failure (the underlying call already swallows the error); reserve real 500s for unexpected exceptions.

Client-side, teach usePost to honour the response body's success flag so success:false flips the toast to error without throwing through axios. Also tidy the toast container: drop icons, no wrapping, single-line ellipsis with a sensible max-width that hugs short messages.
2026-04-26 01:56:13 -04:00
gorkem-bwl 127ee5508a fix(client): truncate long notification destinations with a middle ellipsis
The destination column wrapped onto two lines for long webhook URLs. Cap the cell at 320px and use rtl + plaintext bidi so the start and end of the URL stay visible with an ellipsis in between; full address is in the title attribute.
2026-04-26 01:56:05 -04:00
gorkem-bwl d71fbf8c06 fix(client): bump dark-mode selected toggle background so it stands out
The 6% primary tint that read fine in light mode disappeared against dark backgrounds. Use a 22% tint in dark mode (30% on hover) and keep the lighter 6/10% in light mode.
2026-04-26 01:55:58 -04:00
gorkem-bwl aa3e72ec6d fix(client): make confirmation modals dismiss on ESC
Wire the dialog's onClose to onCancel for keyboard escape but ignore backdrop clicks so users can't lose work by clicking outside the modal.
2026-04-26 01:55:49 -04:00
gorkem-bwl 9d7f4929ad fix(client): use native language names in the settings dropdown too
Export the languageNames map and reuse it in the Settings page so EN/DE/TR turn into English/Deutsch/Türkçe alongside the auth-page selector.
2026-04-26 01:55:42 -04:00
gorkem-bwl 319f667028 feat(client): unify section headers on /logs and group titles with their tables
- Apply the eyebrow variant to "Runtime stats" and "Memory & CPU" so /logs sections share the same uppercase header style as Job queue
- Pair each section's heading + explainer with its table in a tighter sub-stack so the big gap only sits between sections, not under the explainer
- Restore the missing useTheme() call inside TableFailedJobs and TableLogs so the monospace token resolves
2026-04-26 01:55:33 -04:00
gorkem-bwl bcab1a0a4d fix(client): swap the sidebar logs icon from Database to ScrollText
ScrollText reads as logs/journal; Database read more like a storage admin tool.
2026-04-26 01:55:25 -04:00
gorkem-bwl 11ea7dcb3b feat(client): add a sidebar-specific accent colour for dark mode
The sidebar selected-icon colour was sharing primary.main, which read too dark in dark mode. Introduce palette.sidebar.accent (brand green in light mode, a lighter green in dark mode) and route NavItem through it so primary stays brand-green for buttons and the rest of the UI.
2026-04-26 01:55:17 -04:00
gorkem-bwl 1bff81c6bb fix(client): tighten the toast appearance
- Drop the per-type icon
- Lower min-height to 40px and tighten padding
- Thin both the active progress bar and its track to 2px and pin them to the same baseline so they sit on top of each other
2026-04-26 01:32:05 -04:00
gorkem-bwl 2016a208f2 fix(client): keep Chrome autofill background within the rounded input
The autofill inset shadow was bleeding past the rounded corners. Clip it via overflow: hidden on the outlined input root and bump the override rules to !important so they beat Chrome's user-agent styles.
2026-04-26 01:32:00 -04:00
gorkem-bwl dfc1b65e4b fix(client): restyle the register page setup notice
Drop the default MUI info-blue alert in favour of a neutral panel: theme-driven action.hover background, divider border, secondary text colour and no icon. Reads as a quiet aside instead of a status message.
2026-04-26 01:31:54 -04:00
gorkem-bwl f7629eb1be refactor(client): show native language names instead of country flags
Replace the flag + 2-letter code in the language selector with each language's native name (English, Deutsch, 日本語, ...) and drop the unused flag-icons dependency.
2026-04-26 01:31:47 -04:00
gorkem-bwl 72fc28c1fa fix(client): give auth pages a stable form width
The login/register form used percentage widths that collapsed to ~15% on wide screens, wrapping the "Forgot password" / "Reset password" links onto two lines. Switch to a fixed 400px max-width that just shrinks below it on small screens.
2026-04-26 01:31:41 -04:00
gorkem-bwl da70d07051 chore(client): document MUI default fallbacks in getResponseColor
Annotate the hex literals used when callers omit theme colours so the intent (defensive MUI default match) is obvious instead of looking like arbitrary values.
2026-04-26 01:22:06 -04:00
gorkem-bwl c6db7e6479 refactor(client): centralise monospace font through the theme
Add a fontFamilyMonospace token to theme.typography (ui-monospace stack with sensible fallbacks) and route the four ad-hoc fontFamily="monospace" call sites through it so the project has one source of truth.
2026-04-26 01:22:01 -04:00
gorkem-bwl 82ad40ac82 fix(client): replace hardcoded blue accents in tooltip and toast
- Tooltip backgrounds now pull from the theme's grey palette instead of a hand-rolled blue gradient (visible bug in dark mode)
- Toast progress bars use theme.palette.primary.main so they match the rest of the brand
2026-04-26 01:21:54 -04:00
gorkem-bwl 4fd9b2ecf5 feat(client): add eyebrow typography variant and lighter page titles
- Introduce a reusable eyebrow Typography variant in the theme (13px, 500 weight, uppercase, 0.08em tracking) for section headings
- Apply it to ConfigBox titles in /settings and to "Job queue" / "Failed jobs" in /logs
- Drop the page header weight from 600 to 400 with tighter tracking so titles read lighter and more refined
2026-04-26 01:21:47 -04:00
gorkem-bwl 2dccf73050 fix(client): green-tone the empty state illustration
The foreground card was painted in blue (#EEF4FE/#7DA8E8 etc.); recoloured to the brand green family so the illustration matches the rest of the UI.
2026-04-26 01:21:11 -04:00
gorkem-bwl d6532c60a6 fix(client): brand primary colour is the brand green, not blue
Renames the primary palette token from blueBlueWave to brandGreen and points it at #13715B in both light and dark themes, so primary buttons, sidebar selection, slider rings, switch tracks etc. follow the brand instead of the old blue.
2026-04-26 01:21:04 -04:00
gorkem-bwl 99be8651aa feat(client): unify brand mark — green king across sidebar, page header and favicon
- Replace the blue chess-piece SVG with a green king silhouette adapted from Cburnett's chess set (Wikimedia Commons, CC BY-SA 3.0); applies to the page-header logo and the browser favicon
- Drop the placeholder green "C" square in the sidebar in favour of the same king SVG
- Add a NOTICE file documenting the asset attribution and license
2026-04-26 01:07:16 -04:00
gorkem-bwl 8ca53bd765 fix(client): smaller, lighter labels on uptime status cards
Up/down/paused/initializing labels now use the same uppercase 11px treatment as the table headers, with looser tracking, instead of the heavier h2 variant.
2026-04-26 01:07:10 -04:00
gorkem-bwl 26e49f6161 fix(client): drop card chrome around the settings save button
The sticky save bar no longer renders a paper background, top border or full-width margin extension — it just sticks to the bottom on its own.
2026-04-26 00:42:16 -04:00
gorkem-bwl cca027b5eb fix(client): subtle tinted selection on toggle button group
Selected segment uses a 6% primary alpha rather than the previous green tint, and the per-button border override is gone so adjacent segments share one clean divider line.
2026-04-26 00:42:11 -04:00
gorkem-bwl 51f9c09132 feat(client): unify switch, radio and slider styling via theme overrides
- Switch: 36x20 contained pill with 6px right padding so labels keep their distance even inside Stacks
- Radio: theme-level override, 20px ring with a fixed-size inner dot — checked state no longer animates a scale change that pushed labels around
- Slider: white thumb with primary-colour ring, 4px track, soft hover/active glow
- Drop the custom RadioInput icon overrides that bypassed the theme and caused the size jiggle
2026-04-26 00:32:18 -04:00
gorkem-bwl 897204ead3 refactor(client): extract useClientPagination hook and clean up Select prop
- Replace duplicated page/rowsPerPage state in Team, Notifications and Status Pages tables with a shared useClientPagination hook
- Drop unused placeholderColor prop from Select; placeholder always uses secondary text
- Restore LAYOUT.MD constant for the account menu item gap
2026-04-26 00:22:14 -04:00
gorkem-bwl 25735df0c6 fix(client): use theme divider for table header line so it shows in dark mode 2026-04-26 00:18:45 -04:00
gorkem-bwl e3b6183e00 fix(client): soften empty state typography and copy
Drop the heavy 600-weight title in favour of regular-weight secondary text, and reword "Nothing here" to "Nothing to show yet".
2026-04-26 00:18:41 -04:00
gorkem-bwl b2741ad567 fix(client): increase internal padding on status summary cards 2026-04-26 00:18:37 -04:00
gorkem-bwl 7905e97a84 fix(client): match account menu icon style with sidebar nav icons 2026-04-26 00:13:35 -04:00
gorkem-bwl b7d719132d fix(client): use secondary text color for empty select placeholders
Disabled-text was too pale for filter dropdowns like Type/Status/State, where the placeholder doubles as the field label.
2026-04-26 00:13:32 -04:00
gorkem-bwl 210e03cea0 fix(client): refine table header divider and typography
- replace heavier per-cell header borders with one lighter row divider
- shrink header text to 11px with looser tracking for a more refined feel
- tighten failed-jobs labels: "Fail count" -> "Count", "Last failed at" -> "Last failed"
2026-04-26 00:13:27 -04:00
gorkem-bwl 995fc59299 feat(client): add pagination to team, notifications and status page tables 2026-04-26 00:13:23 -04:00
gorkem-bwl cbd8236cb4 chore(i18n): seed jobQueueExplainer and failedJobsExplainer for non-English locales 2026-04-26 00:13:19 -04:00
gorkem-bwl 095086cdfd chore(i18n): seed translations for new UI surfaces
Across en + 15 locales:
- pages.<X>.fallback.title / .description for every page that has an
  EmptyState (uptime, infrastructure, pageSpeed, notifications,
  maintenance, statusPages, incidents, logs, logs.queueFallback,
  checks, account.team).
- pages.<X>.header.title / .description for every page that uses the
  new PageHeader (same list, plus settings).
- pages.logs.diagnostics.sections.runtimeStats / .memoryCpu titles
  and descriptions for the diagnostics section explainers.
- components.pageHeader.links.support / docs / changelog tooltip
  labels.

en.json also gains pages.logs.jobQueueExplainer and
.failedJobsExplainer (English only — non-English locales fall back
to English until a translator sweep).
2026-04-25 23:28:51 -04:00
gorkem-bwl 59846a041f fix(client): dark-mode text-field polish
- MuiOutlinedInput hover override: was inheriting MUI's default
  white outline in dark mode (too bright). Now uses an alpha-23%
  white in dark and text.primary in light.
- TextInput: -webkit-autofill now repaints the input bg with
  background.paper (instead of Chrome's harsh blue/yellow autofill
  highlight), text in text.primary, with the standard 5000s-transition
  trick to suppress the brief flash. Works in both modes.
2026-04-25 23:28:32 -04:00
gorkem-bwl 7d91d9e28b fix(client): job queue table layout + theme-aware running-row tint
TableJobs:
- Cut from 11 to 7 columns, dropping active / runCount / failCount /
  lastFinishedAt (covered by Failed jobs table or by lastRun + lastRunTook).
- All cells get whiteSpace:nowrap + textOverflow:ellipsis + maxWidth so
  long timestamps and IDs no longer wrap and inflate row height; full
  value visible via title tooltip.
- Running-row tint: was theme.palette.success.light (too saturated in
  light, drowns white text in dark). Now #ECF7F2 in light, an 18%-alpha
  green in dark — readable in both modes.

TabQueue:
- One-line explainer above the Job queue heading, with the words
  "Highlighted rows" wrapped in the same green tint so the meaning
  is immediately associated.
- One-line explainer above Failed jobs heading too.
2026-04-25 23:27:50 -04:00
gorkem-bwl 752f59b09b feat(client): diagnostics page polish
- Stats and StatGauges use CSS grid (5 + 4 columns) so cards in each
  row are equal width; widths overridden against the legacy 25%-flex
  GradientBox so they fill their grid cell.
- DetailGauge gains a maxWidth prop (default 225) so consumers in
  wide grids can opt out of the cap.
- Section explainers above each row: "Runtime stats" + description,
  "Memory & CPU" + description (i18n driven, propagated to all 16
  locales via pages.logs.diagnostics.sections).
- Both grids render immediately with placeholder "—" values, so the
  initial paint shows the layout and values fill in as data loads
  (eliminates the layout shake when switching to the tab).
2026-04-25 23:27:35 -04:00
gorkem-bwl 262c4eee93 feat(client): PageHeader on every page; remove sidebar bottom menu
- New PageHeader component (variant D — grouped icon-pill toolbar):
  big title + muted description on the left, Support / Docs /
  Changelog as a 3-icon segmented pill on the right with tooltips.
  External links open in a new tab.
- BasePage / BasePageWithStates / MonitorBasePageWithStates accept
  a headerKey prop; when set, render PageHeader pulling
  pages.<headerKey>.header.title and .description from i18n.
  When unset, falls back to the existing breadcrumb (no behavior
  change for not-yet-migrated pages).
- All top-level pages (uptime, infrastructure, pageSpeed,
  notifications, maintenance, statusPages, incidents, logs, checks,
  account, settings) now pass their headerKey.
- Sidebar: removed the bottom menu (Support / Discussions / Docs /
  Changelog) since those links now live in the page header.
2026-04-25 23:27:22 -04:00
gorkem-bwl 031a6fdf3c feat(client): unified EmptyState across all pages and table empties
- New EmptyState + EmptyStateIllustration components: stacked-cards SVG
  with grid background, theme-aware light/dark palette.
- Variants: default (centered with title + description), compact
  (horizontal, in-section), fullscreen (vertically centers in viewport).
- Wired into Checks, Incidents, Logs (Logs+Queue tabs), Notifications,
  Maintenance, Status pages, and Team tab — each with i18n title +
  description and a fullscreen layout when truly empty.
- Table.EmptyView now wraps the EmptyState in a divider-bordered card
  so short messages no longer look orphaned.
- Incidents Option-A short-circuit: when summary.total === 0, replace
  the whole page chrome with EmptyState; otherwise keep filters + cards.
- Fallback.tsx (BaseFallback / EmptyFallback / EmptyMonitorFallback /
  ErrorFallback) refactored to delegate to EmptyState.
2026-04-25 23:26:34 -04:00
gorkem-bwl ff208710d7 feat(client): unified EmptyState component for empty/error placeholders
Replaces the cards-with-halo SkeletonCard fallback (which produced a
washed-out, hard-to-read placeholder) with a single canonical empty
state used everywhere in the app.

What's new:

- EmptyStateIllustration.tsx — custom inline SVG: two stacked monitor
  cards (front blue, back gray, both with hairline strokes and a soft
  fade toward the bottom), set against a faint grid of solid + dashed
  background lines. Uses only lightened palette tokens (#7DA8E8 blue,
  #B6C2CF gray, #CBD5E1 grid).
- EmptyState.tsx — the layout: illustration + title + description +
  optional CTA + optional secondary link + optional inline alert slot.
  Supports a compact horizontal variant for in-panel use.

Migrations:

- Fallback.tsx: BaseFallback / EmptyFallback / EmptyMonitorFallback /
  ErrorFallback now delegate to EmptyState. Public APIs unchanged so
  every existing consumer (BasePageWithStates, MonitorBasePageWithStates,
  Pages/Uptime, Pages/Notifications, Pages/Infrastructure, etc.) keeps
  working without edits.
- BasePage.tsx: PageSpeedKeyPriorityFallback rebuilt on top of EmptyState
  with the warning slot. Fixes the previous reports of "white cloudy
  area" + half-faded text — message and link now render in consistent
  dark text with green Settings link.

No prop changes for any page-level consumer. The illustration is a
single SVG with no external assets.
2026-04-25 21:19:52 -04:00
Gorkem Cetin 29f1041cc3 Merge pull request #3578 from ZeinebMegaadi/fix/docs-link
fix:replaced the broken link with the working one everywhere
2026-04-25 19:40:31 -04:00
ZeinebMegaadi a613bfb45a fix:replaced the broken link with the working one everywhere 2026-04-25 15:33:42 +01:00
Gorkem Cetin ed49fd41db Merge pull request #3575 from ayshaL/fix/notification-channel-typo
Fixed typo in notification
2026-04-24 19:17:58 -04:00
Aicha Lassoued 625f9b588e Fix typo in heapAllocation string 2026-04-24 22:45:45 +01:00
Aicha Lassoued 6fa73dce49 fixed typo 2026-04-24 22:24:00 +01:00
Alexander Holliday e8892af8ba Merge pull request #3573 from bluewave-labs/fix/swagger-monitors-teams-args
docs(openapi): document all query params on /monitors/team/with-checks
2026-04-24 10:43:34 -07:00
Alexander Holliday e413991e63 Merge pull request #3571 from bluewave-labs/fix/check-schema-errors-warning
fix(server): suppress mongoose reserved-key warning on Check.errors
2026-04-24 10:19:03 -07:00
Alexander Holliday e909ddd78b Merge pull request #3572 from bluewave-labs/fix/version-from-package-json
fix(client): source app version from package.json
2026-04-24 10:12:20 -07:00
Alexander Holliday 2ed5015aee Merge pull request #3570 from bluewave-labs/fix/vite-allowed-hosts
fix(client): allow configurable Vite allowed hosts
2026-04-24 09:58:27 -07:00
Alexander Holliday 6e89be2ac5 Merge pull request #3574 from bluewave-labs/fix/open-site-action-internal-url
fix(client): hide Open site action when URL is not browser-openable
2026-04-24 09:51:01 -07:00
gorkem-bwl 50fb4f47fd fix(client): hide Open site action when URL is not browser-openable
The Open site action in monitor row menus called `window.open(monitor.url)`
unconditionally, which produced unresolvable navigation for monitors
targeting internal services:
- Hardware monitors always point at a Capture agent (e.g.
  http://capture:59232/api/v1/metrics) that the user's browser can't
  reach.
- Port, ping, docker, game, grpc, and websocket monitors store hostnames
  or non-HTTP endpoints, which browsers can't load anyway.

This change hides the action where it can't possibly work:
- Removed entirely from the Infrastructure (hardware) monitors table.
- In the Uptime monitors table, only shown for http and pagespeed
  monitor types whose URL starts with http:// or https://.

A more comprehensive fix that adds a separate user-facing siteUrl
field on the Monitor schema is left for a follow-up.

Closes #3434
2026-04-23 23:38:07 -04:00
gorkem-bwl 22f1d035b7 docs(openapi): document all query params on /monitors/team/with-checks
The OpenAPI spec only listed `limit` for GET /monitors/team/with-checks,
but the route's Zod schema (getMonitorsWithChecksQueryValidation) also
accepts page, rowsPerPage, filter, field, order, type, and explain.
Users hitting the endpoint via Swagger UI would either get rejected
with "page is not allowed" (when guessing param names) or miss
pagination entirely.

Adds the missing parameters with constraints mirrored from the
validation schema. Also bumps `limit` max from 50 to 100 to match
the schema (was 100 in code, 50 in spec).

Closes #3354
2026-04-23 23:35:52 -04:00
gorkem-bwl f654310c4d fix(client): source app version from package.json
The client version shown on the Settings page was hardcoded as a
literal in vite.config.ts, while client/package.json sat at the
placeholder "0.0.0". Bumping the displayed version required editing
two unrelated places, and stale Docker client images would keep
showing the old number after upgrades.

This change makes client/package.json the single source of truth
and reads it at build time. Bumps client/package.json from 0.0.0
to 3.6.1 to preserve current behaviour.

Closes #3271
2026-04-23 23:33:41 -04:00
gorkem-bwl d44f7497a5 fix(server): suppress mongoose reserved-key warning on Check.errors
Mongoose 8 logs a reserved-key warning at boot because the Check schema
defines an `errors` field, which collides with Mongoose's internal
document `errors` accessor. The field is intentional and safe to keep,
so opt out of the warning via `suppressReservedKeysWarning: true`.

Closes #1539
2026-04-23 23:32:33 -04:00
gorkem-bwl 944ee74a1d feat(status-page): four selectable themes for the public status page
Adds four fully-designed visual themes for the public status page
(refined | modern | bold | editorial) with a light/dark mode selector,
admin preview in a mock browser frame, and a shared theme provider
that keeps the component tree unified while letting each theme own
its visual identity.

## Selectable themes
- Refined — clean, flat cards, soft shadows, colored status dot.
- Modern — flat surfaces with a pulsing status dot and staggered
  fade-in cards.
- Bold — conic-gradient logo, 34px hero title with a check disc,
  subtle dual-radial glow on dark mode, accent-green hover border.
- Editorial — serif headings, narrow newspaper column, 'Service
  Report' eyebrow + title, flat rectangular badges and dateline.

## Architecture
- Single `StatusPageThemeProvider` hosts CSS custom-property tokens
  per theme and resolves light/dark based on admin-forced mode,
  falling back to `prefers-color-scheme` with a matchMedia listener.
- Each theme owns only its `*StatusPage.tsx` + scoped CSS file.
  The heatmap, histogram, and infrastructure renderers are shared
  across all four themes (parameterized by a `classPrefix`) to avoid
  the 12-file duplication the initial draft had.
- Renderer lookup table in `Status/index.tsx` replaces a 4-way
  ternary chain; unknown themes fall through to the legacy renderer.
- `resolveOverallStatus`, `statusBadgeKey`, `monoFirstChar`, and
  `monitorBadgeTone` are extracted to `themes/shared/overallStatus.tsx`.

## Admin UX
- `ThemePicker` uses the project's `RadioWithDescription` and
  `Select` components — one radio card per theme plus an Auto/
  Light-only/Dark-only mode dropdown.
- `BrowserFrame` wraps the preview in the admin panel with a mock
  chrome (traffic lights + URL bar) that adopts the status page's
  theme tokens. Edge-to-edge fill with no gray bleed.
- Public route renders the theme directly on the viewport with the
  document body painted to the theme background, reverted on unmount.

## Performance & polish
- Histogram's derived stats (padded array, max, avg, peak) are
  memoized on `checks`; `max` is reused for `peak` instead of
  computing twice.
- Editorial's dateline is wrapped in `useMemo` so the 10s poll
  doesn't rerender "April 23, 2026" as a changing string.
- Provider's `setSystemMode` reconciliation uses the updater form
  to avoid an extra render when the value is unchanged.
- `monoFirstChar` no longer uses a non-null assertion.

## Form plumbing
- `useStatusPageForm` defaults include `theme` + `themeMode`.
- `Validation/statusPage.ts` accepts both as optional zod enums.
- `Pages/StatusPage/Create/index.tsx` appends both to FormData on
  create and update.

## CLAUDE.md
- Dev port note: local client runs on 10001 (5173 is used by a
  different local project); server `CLIENT_HOST` is set to match.
2026-04-23 22:51:40 -04:00
gorkem-bwl ad964446de feat(status-page): add theme and themeMode fields (backend)
## Changes
- Add `theme` (refined | modern | bold | editorial, default "refined") and
  `themeMode` (auto | light | dark, default "auto") fields to the StatusPage
  Mongoose model and the shared `StatusPage` type.
- Extend the create/update Zod validator to accept both fields optionally.
- Map both fields in the repository's `toEntity` so GET responses include them.
- Document both fields in the create and update endpoints in `openapi.json`.
- Add `STATUS_PAGE_THEMES_ENABLED` env var (default `true`) wired through
  `SettingsService` for ops-level rollback. When disabled, the controller
  strips theme fields on write and forces defaults on read.

## Notes
- No migration needed; Mongoose applies defaults for legacy documents.
- Controller short-circuits to zero-cost when the feature flag is on.
- `DEFAULT_STATUS_PAGE_THEME` / `DEFAULT_STATUS_PAGE_THEME_MODE` constants
  exported so the frontend PR can consume the same values.
2026-04-23 22:45:50 -04:00
ZeinebMegaadi d41d9a3dab fix: skip future date validation when editing maintenance window 2026-04-18 12:55:10 +01:00
ZeinebMegaadi 1a54a24fd5 fix: skip future date validation when editing maintenance window 2026-04-18 12:54:13 +01:00
mannilakash ac51dfeab7 fix: removing default value for strategy - monitors 2026-04-13 13:17:24 -04:00
mannilakash 99a5aa1722 fix: badge and timescaleDB changes 2026-04-13 08:29:12 -04:00
mannilakash 5791bdd5e7 fix: adding timescaledb changes 2026-04-07 07:25:35 -04:00
mannilakash f7514bc938 fix: format errors 2026-04-07 07:14:17 -04:00
mannilakash 2bbaf7d1eb feat: adding mobile/desktop pagespeed strategy 2026-04-07 07:02:44 -04:00
Anton Mrosek 726e4de6ab Compose dist-arm: Removed mongodb port mapping 2026-02-16 12:31:44 +01:00
576 changed files with 48369 additions and 14431 deletions

No files matched your search

Regular → Executable
+21 -2
View File
@@ -1,4 +1,23 @@
#!/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 && cd ..
cd server && npx lint-staged && cd ..
(cd client && npx lint-staged) || exit 1
(cd server && npx lint-staged) || exit 1
+45 -18
View File
@@ -12,20 +12,22 @@ Checkmate is an open-source uptime and infrastructure monitoring application. It
```bash
cd client
npm install
npm run dev # Start dev server at http://localhost:5173
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 Mocha tests with c8 coverage
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
@@ -66,19 +68,23 @@ VITE_APP_LOG_LEVEL="debug"
### Backend Layers
```
server/src/
├── controllers/ # Route handlers (authController, monitorController, etc.)
├── service/ # Business logic
│ ├── business/ # Core monitoring logic
│ ├── infrastructure/ # Server/system utilities
│ └── system/ # App-level settings
├── 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/
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
│ ├── migration/ # Database migrations (run on startup)
│ └── modules/ # Database-specific modules
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
├── routes/v1/ # API route definitions
├── validation/ # Joi input validation schemas
└── repositories/ # Data access layer
│ └── migration/ # Database migrations (run on startup)
├── templates/ # Email/notification templates
├── types/ # Shared const tuples + derived types
└── utils/ # Shared utilities
```
### Frontend Structure
@@ -157,6 +163,26 @@ When working on anything related to check scheduling, incident lifecycle, or not
## 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
@@ -174,12 +200,13 @@ t('your.key') // Never hardcode UI strings
- Both use ESLint with strict settings
### Testing
Server tests use Mocha + Chai + Sinon:
Server tests use Jest (with `--experimental-vm-modules` for ESM):
```bash
npm test # Run all tests with coverage
npm test -- --grep "pattern" # Run specific tests
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/tests/**/*.test.js`
Test files: `server/test/**/*.test.ts`
## Database Models
+5 -18
View File
@@ -151,6 +151,7 @@ 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:
@@ -240,7 +241,7 @@ docker rm uptime_database_mongo
**Need more help?**
- Check the [full documentation](https://docs.checkmate.so)
- Check the [full documentation](https://checkmate.so/docs)
- Ask on [Discord](https://discord.com/invite/NAb6H3UTjK)
### Start contributing code?
@@ -255,20 +256,6 @@ docker rm uptime_database_mongo
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
### Improve the documentation?
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?
We use [PoEditor](https://poeditor.com) for translations. You can:
- [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.
Make sure all new UI strings in code use `t('key')`.
### Submit a pull request?
Follow the [pull request checklist](#pull-request-checklist). Your PR should:
@@ -283,7 +270,7 @@ Follow the [pull request checklist](#pull-request-checklist). Your PR should:
## Code guidelines
- Use ESLint and Prettier (`npm run lint`).
- 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.
- 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.
@@ -304,7 +291,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 linting and has no TypeScript errors.
- Your code passes `npm run lint`, `npm run format-check`, and `npm run build` in both `client` and `server` with no errors.
If one or more of these are missing, we may ask you to update your pull request before we can merge it.
@@ -316,7 +303,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://docs.checkmate.so) covers it.
- Make sure you run the code locally. The Checkmate [documentation](https://checkmate.so/docs) 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.
-172
View File
@@ -1,172 +0,0 @@
<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/checkmate/discussions)
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
## 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.
+40 -28
View File
@@ -1,5 +1,26 @@
<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)
@@ -7,7 +28,6 @@
![](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 Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![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>
@@ -16,7 +36,10 @@
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate)
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
<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.
@@ -24,8 +47,6 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
## 📚 Table of contents
- [📦 Demo](#demo)
@@ -38,15 +59,16 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
- [🏗️ Tech stack](#tech-stack)
- [🔗 A few links](#a-few-links)
- [🤝 Contributing](#contributing)
- [💰 Our sponsors](#our-sponsors)
<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).
@@ -55,6 +77,7 @@ Usage instructions can be found [here](https://checkmate.so/docs).
- [Docker](https://www.docker.com/) installed
- [Git](https://git-scm.com/) installed
<a id="installation"></a>
## Installation
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
@@ -67,6 +90,7 @@ If you need to monitor internal HTTPS endpoints with certificates from private C
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:
@@ -77,6 +101,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
![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:
@@ -86,33 +111,21 @@ If you have any questions, suggestions or comments, you have several options:
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)
- Website monitoring
- Several monitoring options: Uptime, Docker, Ping, SSL, Port, 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
- Docker monitoring
- Ping monitoring
- SSL monitoring
- Port monitoring
- Game server monitoring (3.0)
- Incidents at a glance
- Status pages
- E-mail, Webhooks, Discord and Slack notifications
- Status pages with 4 beautiful themes
- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS) notifications
- Scheduled maintenance
- JSON query monitoring
- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan)
- Multi-language support for Arabic, Chinese (Simplified), Chinese (Traditional, Taiwan), Czech, English, Finnish, French, German, Japanese, Portuguese (Brazil), Russian, Spanish, Thai, Turkish, Ukrainian, and Vietnamese
**Short term roadmap:**
- Plugins that will help Checkmate get any information from a remote service (e.g database, etc)
- Better notifications
- Network monitoring
- ..and a few more features
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
## Monitor Lifecycle
@@ -123,6 +136,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
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>
@@ -140,6 +154,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
<a id="tech-stack"></a>
## Tech stack
- [ReactJs](https://react.dev/)
@@ -149,6 +164,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
- [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".
@@ -156,6 +172,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
- 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), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers.
@@ -180,8 +197,3 @@ Here's how you can contribute:
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date)
## Our sponsors
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: checkmate-chart
description: A Helm chart for Checkmate App
type: application
version: 0.1.0
appVersion: "2.3"
version: 0.2.0
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
+142 -12
View File
@@ -19,12 +19,11 @@ cd checkmate/charts/helm/checkmate
### 2. Customize values.yaml
Edit `values.yaml` to update:
- `client.ingress.host` and `server.ingress.host` with your domain names
- `server.protocol` (usually http or https)
- `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`
- Old: `persistence.redisSize` → New: `persistence.redis.size`
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
- 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)
@@ -32,7 +31,7 @@ Edit `values.yaml` to update:
```bash
helm install checkmate ./charts/helm/checkmate
```
This will deploy the client, server, MongoDB, and Redis components.
This will deploy the client, api, and MongoDB components.
### 4. Verify the deployment
Check pods and services:
@@ -43,6 +42,137 @@ 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.
@@ -68,7 +198,7 @@ client:
enabled: true
secretName: checkmate-client-tls
server:
api:
protocol: https
ingress:
enabled: true
@@ -78,7 +208,7 @@ server:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-server-tls
secretName: checkmate-api-tls
```
### Alternative: Using --set flags
@@ -88,13 +218,13 @@ You can also enable TLS during installation using Helm's `--set` flags:
```bash
helm install checkmate ./charts/helm/checkmate \
--set client.protocol=https \
--set server.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 server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set server.ingress.tls.enabled=true \
--set server.ingress.tls.secretName=checkmate-server-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
@@ -107,7 +237,7 @@ kubectl get certificates
# Check certificate details
kubectl describe certificate checkmate-client-tls
kubectl describe certificate checkmate-server-tls
kubectl describe certificate checkmate-api-tls
# Verify the secrets were created
kubectl get secrets | grep checkmate-tls
@@ -0,0 +1,35 @@
{{- /*
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 -}}
@@ -0,0 +1,65 @@
{{- $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,11 +1,12 @@
{{- if .Values.server.ingress.enabled }}
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if $api.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkmate-server-ingress
{{- if .Values.server.ingress.annotations }}
name: checkmate-api-ingress
{{- if $api.ingress.annotations }}
annotations:
{{- range $key, $value := .Values.server.ingress.annotations }}
{{- range $key, $value := $api.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
@@ -17,22 +18,22 @@ metadata:
#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: {{ .Values.server.ingress.className }}
{{- if .Values.server.ingress.tls.enabled }}
ingressClassName: {{ $api.ingress.className }}
{{- if $api.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.server.ingress.host }}
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
- {{ $api.ingress.host }}
secretName: {{ default (printf "%s-api-tls" .Release.Name) $api.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.server.ingress.host }}
- host: {{ $api.ingress.host }}
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: checkmate-server
name: checkmate-api
port:
number: {{ .Values.server.port }}
number: {{ $api.port }}
{{- end }}
@@ -0,0 +1,10 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: Service
metadata:
name: checkmate-api
spec:
selector:
app: checkmate-api
ports:
- port: {{ $api.port }}
@@ -1,3 +1,4 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -22,12 +23,13 @@ spec:
{{- end }}
containers:
- name: client
image: {{ .Values.client.image }}
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: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
value: "{{ $api.protocol }}://{{ $api.ingress.host }}/api/v1"
- name: UPTIME_APP_CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
volumeMounts:
@@ -41,4 +43,4 @@ spec:
volumes:
- name: config-volume
configMap:
name: checkmate-server-nginx-cm
name: checkmate-client-nginx-cm
@@ -1,7 +1,8 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: ConfigMap
kind: ConfigMap
metadata:
name: checkmate-server-nginx-cm
name: checkmate-client-nginx-cm
data:
default.conf: |
server {
@@ -22,7 +23,7 @@ data:
}
# location /api/ {
# proxy_pass http://{{ .Values.server.ingress.host }}:5000/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;
@@ -31,7 +32,7 @@ data:
# }
location /api-docs/ {
proxy_pass http://{{ .Values.server.ingress.host }}:5000/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;
@@ -22,7 +22,8 @@ spec:
{{- end }}
containers:
- name: mongodb
image: {{ .Values.mongodb.image }}
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"]
@@ -37,7 +38,9 @@ spec:
- metadata:
name: checkmate-mongo-persistent-storage
spec:
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
{{- with .Values.persistence.mongo.storageClass }}
storageClassName: {{ . | quote }} {{/* omitted when empty so the cluster's default StorageClass applies */}}
{{- end }}
accessModes: ["ReadWriteOnce"]
resources:
requests:
+20 -14
View File
@@ -1,14 +1,15 @@
{{- $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 .Values.server.ingress.host "change_me" }}
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
{{- 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 }}
{{- $serverProtocol := .Values.server.protocol }}
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
{{- fail "server.protocol must be either 'http' or 'https'" }}
{{- $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 }}
@@ -21,28 +22,33 @@
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
{{- end }}
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
{{- 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 $serverProtocol "https") }}
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
{{- 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 }}
{{- $sAnn := .Values.server.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") }}
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "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 .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- 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 }}
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
{{/* 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,11 +0,0 @@
{{- if .Values.redis.enabled }}
apiVersion: v1
kind: Service
metadata:
name: checkmate-redis
spec:
selector:
app: checkmate-redis
ports:
- port: {{ .Values.redis.port }}
{{- end }}
@@ -1,33 +0,0 @@
{{- if .Values.redis.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: checkmate-redis
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-redis
template:
metadata:
labels:
app: checkmate-redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
ports:
- containerPort: {{ .Values.redis.port }}
volumeMounts:
- name: redis-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: checkmate-redis-persistent-storage
spec:
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
{{- end }}
@@ -4,6 +4,14 @@
{{- 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:
@@ -11,5 +19,9 @@ metadata:
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,37 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-server
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-server
template:
metadata:
labels:
app: checkmate-server
spec:
{{- with .Values.server.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.server.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: server
image: {{ .Values.server.image }}
ports:
- containerPort: {{ .Values.server.port }}
envFrom:
- secretRef:
name: checkmate-secrets
{{- with .Values.server.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
env:
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: checkmate-server
spec:
selector:
app: checkmate-server
ports:
- port: {{ .Values.server.port }}
@@ -0,0 +1,66 @@
{{- $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 }}
@@ -0,0 +1,30 @@
{{- 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 }}
+64 -26
View File
@@ -1,5 +1,7 @@
client:
image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0
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:
@@ -17,10 +19,24 @@ client:
# 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
server:
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
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
@@ -31,40 +47,62 @@ server:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
# Note: when enabling TLS, also set server.protocol: https and add
# 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
redis:
enabled: false
image: redis:7.2
port: 6379
mongodb:
image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0
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
# REFRESH_TOKEN_SECRET: change_me
# SYSTEM_EMAIL_ADDRESS: test@example.com
# SYSTEM_EMAIL_PASSWORD: change_me
# SYSTEM_EMAIL_HOST: smtp.example.com
# SYSTEM_EMAIL_PORT: "587"
# PAGESPEED_API_KEY: change_me
DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db
DB_CONNECTION_STRING: mongodb://checkmate-mongodb:27017/uptime_db
CLIENT_HOST: change_me
# REDIS_HOST: redis
# REDIS_PORT: "6379"
# DB_TYPE: MongoDB
# TOKEN_TTL: 99d
# REFRESH_TOKEN_TTL: 99d
NODE_ENV: production
LOG_LEVEL: info
TOKEN_TTL: 99d
persistence:
mongo:
size: 5Gi
storageClass: ""
redis:
size: 1Gi
storageClass: ""
+135
View File
@@ -1,3 +1,104 @@
// 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 },
@@ -15,9 +116,43 @@ module.exports = {
"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,
},
},
],
};
+18
View File
@@ -0,0 +1,18 @@
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.
+931 -218
View File
File diff suppressed because it is too large. Load diff
+11 -7
View File
@@ -1,21 +1,23 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"version": "3.9.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build-dev": "tsc -b && vite build --mode development",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"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 ."
},
"lint-staged": {
"**/*": "prettier --write"
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
@@ -26,13 +28,12 @@
"@mui/x-date-pickers": "7.29.4",
"@reduxjs/toolkit": "2.7.0",
"@types/maplibre-gl": "^1.13.2",
"axios": "^1.7.4",
"axios": "^1.15.2",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"joi": "17.13.4",
"lucide-react": "^0.562.0",
"maplibre-gl": "^5.19.0",
"mui-color-input": "^7.0.0",
@@ -58,14 +59,17 @@
"@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.1"
"vite": "^6.4.2"
}
}
+10 -48
View File
@@ -1,49 +1,11 @@
<svg width="32" height="32" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M218.252 79.2667L218.045 78.4143C218.712 77.617 219.642 77.5282 220.615 77.4361C224.829 77.0374 243.307 76.7077 246.676 77.418C248.299 77.76 249.661 78.5248 250.932 79.5734C254.845 82.8003 259.018 88.0302 262.403 91.9284C263.57 93.2734 264.805 95.1623 266.183 96.2584C268.203 99.7185 274.532 101.602 278.167 102.623C278.397 102.688 278.631 102.741 278.863 102.797C274.983 106.185 272.016 110.321 268.272 113.745C272.574 124.188 286.689 184.953 289.397 187.986C290.165 188.212 290.291 188.048 291.045 187.739C290.627 188.446 290.337 189.3 289.588 189.656L288.963 188.533C286.703 188.347 273.044 195.497 269.836 196.808C267.39 197.808 264.851 198.458 262.414 199.405C261.197 196.797 260.413 193.923 259.329 191.244C254.43 179.142 250.091 166.843 245.71 154.552C240.241 139.206 234.226 124.026 228.882 108.642C226.389 101.466 223.549 94.5009 220.953 87.3749C220.026 84.8301 219.72 81.4795 218.252 79.2667Z" fill="#3158B7"/>
<path d="M218.252 79.2667L218.045 78.4143C218.712 77.617 219.642 77.5282 220.615 77.4361C224.829 77.0374 243.307 76.7077 246.676 77.418C248.299 77.76 249.661 78.5248 250.932 79.5734C254.845 82.8003 259.018 88.0302 262.403 91.9284C263.57 93.2734 264.805 95.1623 266.183 96.2584C268.203 99.7185 274.532 101.602 278.167 102.623C278.397 102.688 278.631 102.741 278.863 102.797C274.983 106.185 272.016 110.321 268.272 113.745C266.08 111.774 263.588 110.159 261.208 108.427C256.477 104.982 251.692 101.649 246.906 98.282L233.54 88.9767C228.729 85.5908 223.823 81.3267 218.252 79.2667Z" fill="#73E4FA"/>
<path d="M299.595 19.4005C302.177 17.8183 305.226 16.8946 307.955 15.5556C310.387 14.3626 312.673 12.8604 315.15 11.7619C315.624 11.5519 315.886 11.4697 316.398 11.4844C315.732 13.7848 316.431 26.6622 316.441 30.2652C316.474 42.2192 316.084 54.2432 316.385 66.1867C316.416 67.4343 316.377 68.6301 316.847 69.8026C316.457 79.0631 316.718 88.4086 316.651 97.6808L316.423 172.062C316.376 177.75 316.385 183.438 316.448 189.125C316.469 192.585 316.902 196.888 316.126 200.243C316.055 199.033 315.728 197.334 316.025 196.183C315.86 197.435 315.463 198.762 315.391 200.002C315.283 201.847 315.798 203.719 315.391 205.559C314.431 204.554 313.43 203.906 312.592 202.729C308.228 196.602 310.556 194.07 301.963 190.683C299.704 189.793 293.054 186.88 291.045 187.739C290.291 188.048 290.165 188.212 289.397 187.986C286.689 184.953 272.574 124.188 268.272 113.745C272.016 110.321 274.983 106.185 278.863 102.797C278.631 102.741 278.397 102.688 278.167 102.623C274.532 101.602 268.203 99.7184 266.183 96.2583C269.435 97.3504 272.901 97.5882 276.304 97.6583C280.306 97.7408 283.403 97.3026 286.383 94.3544C288.086 92.669 289.516 90.6569 291.136 88.8839C293.303 86.512 296.041 84.1801 297.633 81.3777C300.012 77.1924 299.115 62.4967 299.219 57.0841C296.538 56.7086 293.912 56.806 291.218 56.6995C288.184 56.5796 285.396 56.0507 283.294 53.6585C280.886 50.9188 280.696 47.3466 281.035 43.8756C282.423 29.6657 295.122 37.239 298.804 33.8184C299.712 32.9739 299.469 21.618 299.595 19.4005Z" fill="#3D7ECD"/>
<path d="M362.288 269.421L362.773 270.062C364.332 274.37 365.193 279.073 366.359 283.516C368.419 291.371 370.61 299.194 372.793 307.015L395.769 388.039L411.304 442.863L415.766 458.213C416.528 460.836 417.569 463.467 417.96 466.175C417.89 466.842 418.03 466.869 417.485 467.324C413.349 466.406 409.11 465.92 404.942 465.147C396.892 463.653 388.881 461.984 380.817 460.552C380.516 459.221 380.489 457.712 380.305 456.347C379.896 453.327 379.359 450.33 378.694 447.356C376.712 438.738 374.16 430.227 372.075 421.626L351.327 334.35C349.071 324.725 347.019 314.981 344.607 305.405C342.561 297.284 340.078 289.201 338.622 280.946C339.912 280.649 340.991 280.35 342.142 279.684C345.072 278.136 348.264 277.064 351.221 275.536C354.9 273.636 358.345 270.649 362.288 269.421Z" fill="#73E4FA"/>
<path d="M316.398 11.4845C316.949 11.6168 317.496 11.7682 318.021 11.982C320.527 13.0022 322.595 14.8975 325.025 16.0401C326.641 16.7993 328.506 17.0278 330.066 17.9189C330.615 18.2326 331.175 18.6401 331.516 19.1833C333.638 22.5617 329.941 33.143 334.144 34.5595C335.071 34.872 336.309 34.7269 337.278 34.6636C342.517 34.3216 346.04 34.3235 349.065 39.1827C350.891 42.117 351.841 45.3207 350.982 48.7564C350.164 52.0314 347.831 55.4635 344.902 57.2399C344.309 57.5993 343.657 57.8308 343.003 58.0488C342.382 58.2541 341.729 58.4831 341.176 58.8402C339.34 60.0257 337.705 61.6472 335.85 62.8819C335.083 63.3916 333.536 63.9343 333.114 64.7163C333.015 64.8989 332.938 65.0921 332.849 65.2797C331.994 71.778 330.942 74.7954 335.242 80.4525C338.105 84.2196 345.676 93.407 349.248 96.0711C350.329 96.8768 351.548 97.3899 352.858 97.674C354.638 97.679 356.567 97.8353 358.319 97.4932C366.986 95.8002 372.945 85.6789 379.155 80.1412C380.557 78.8916 382.117 77.8947 383.974 77.4926C387.385 76.754 391.296 77.2429 394.773 77.2627C400.744 77.2966 406.631 77.1509 412.603 77.4939C413.066 77.8038 413.08 77.6663 413.16 78.1487C413.385 79.5036 412.832 81.0938 412.41 82.377C409.805 90.2864 406.49 98.1119 403.607 105.936L387.525 150.29C383.617 161.269 379.569 172.195 375.381 183.07C373.017 189.241 370.826 195.663 367.756 201.518C366.906 200.846 366.105 200.384 365.115 199.946C357.118 196.547 349.468 191.947 341.757 187.926C341.211 187.423 341.102 187.207 340.301 187.318C338.422 187.579 336.346 188.419 334.502 188.929C330.434 190.055 321.796 191.764 319.469 195.652C318.572 197.151 317.692 200.22 316.337 201.245C316.225 200.886 316.145 200.621 316.126 200.243C316.902 196.888 316.469 192.585 316.448 189.125C316.385 183.438 316.377 177.75 316.423 172.062L316.651 97.6809C316.718 88.4087 316.457 79.0632 316.847 69.8027C316.377 68.6302 316.416 67.4344 316.385 66.1868C316.084 54.2433 316.474 42.2193 316.441 30.2653C316.431 26.6623 315.732 13.7849 316.398 11.4845Z" fill="#73E4FA"/>
<path d="M332.127 57.4141C335.751 57.5106 339.378 57.8905 343.003 58.0488C342.382 58.2541 341.729 58.4831 341.176 58.8402C339.34 60.0257 337.705 61.6472 335.85 62.8819C335.083 63.3916 333.536 63.9343 333.114 64.7163C333.015 64.8989 332.938 65.0921 332.849 65.2797C332.684 62.6459 332.422 60.0356 332.127 57.4141Z" fill="black"/>
<path d="M352.858 97.674C354.638 97.679 356.567 97.8353 358.319 97.4932C366.986 95.8002 372.945 85.6789 379.155 80.1412C380.557 78.8916 382.117 77.8947 383.974 77.4926C387.385 76.754 391.296 77.2429 394.773 77.2627C400.744 77.2966 406.631 77.1509 412.603 77.4939C412.123 77.5931 411.776 77.6589 411.35 77.9133C409.284 79.1482 407.188 81.3404 405.081 82.7802C394.559 89.9716 384.283 97.4873 373.787 104.715C371.042 106.605 368.398 108.696 365.587 110.481C363.016 106.042 360.879 102.667 356.684 99.576C355.908 99.004 354.963 98.1848 354.042 97.8844C353.777 97.7979 353.467 97.7915 353.199 97.7482C353.084 97.7297 352.972 97.6988 352.858 97.674Z" fill="#4F91E0"/>
<path d="M316.847 69.8027C321.242 72.4105 326.84 79.7356 330.518 83.5726C335.063 88.3145 340.305 92.4625 345.078 96.9857C348.754 100.47 359.102 112.197 362.531 113.979C362.884 116.416 347.596 173.131 345.411 179.979C344.657 182.345 343.764 185.35 342.254 187.338C342.099 187.543 341.927 187.734 341.757 187.926C341.211 187.423 341.102 187.207 340.301 187.318C338.422 187.579 336.346 188.419 334.502 188.929C330.434 190.055 321.796 191.764 319.469 195.652C318.572 197.151 317.692 200.22 316.337 201.245C316.225 200.886 316.145 200.621 316.126 200.243C316.902 196.888 316.469 192.585 316.448 189.125C316.385 183.438 316.376 177.75 316.423 172.062L316.651 97.6809C316.718 88.4087 316.457 79.0632 316.847 69.8027Z" fill="#5BBFFC"/>
<path d="M316.126 200.243C316.145 200.621 316.225 200.886 316.337 201.245C317.692 200.22 318.572 197.151 319.469 195.652C321.796 191.764 330.434 190.055 334.502 188.929C336.346 188.419 338.422 187.579 340.301 187.318C341.102 187.207 341.211 187.423 341.757 187.926C349.468 191.947 357.118 196.547 365.115 199.946C366.105 200.384 366.906 200.846 367.756 201.518C367.27 202.637 366.763 203.747 366.234 204.848C365.063 207.595 364.344 210.137 363.459 212.952C367.613 213.364 371.797 213.297 375.966 213.194C386.171 213.341 399.134 211.56 407.006 219.438C408.262 220.694 409.583 222.077 410.315 223.707C411.447 228.7 410.939 234.09 410.692 239.173C410.661 239.285 410.628 239.397 410.599 239.511C410.317 240.612 410.264 241.816 409.76 242.846C408.269 245.89 403.918 248.67 401.099 250.509L397.101 250.535C392.229 251.156 386.936 250.764 382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C234.444 250.731 231.884 250.523 229.351 250.403C226.903 249.086 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C227.525 217.021 228.391 216.267 229.547 215.874C232.606 214.832 236.233 214.827 239.433 214.547C243.327 214.206 247.238 213.733 251.141 213.538C256.502 213.269 261.883 213.36 267.25 213.257C267.293 213.167 267.345 213.08 267.378 212.986C268.103 210.948 263.421 201.839 262.414 199.405C264.851 198.458 267.39 197.808 269.836 196.808C273.044 195.497 286.703 188.347 288.963 188.533L289.588 189.656C290.337 189.3 290.627 188.446 291.045 187.739C293.054 186.88 299.704 189.793 301.963 190.683C310.556 194.07 308.228 196.602 312.592 202.729C313.43 203.906 314.431 204.554 315.391 205.559C315.798 203.719 315.283 201.847 315.391 200.002C315.463 198.762 315.86 197.435 316.025 196.183C315.728 197.334 316.055 199.033 316.126 200.243Z" fill="#73E4FA"/>
<path d="M226.648 217.845C227.525 217.021 228.391 216.267 229.547 215.874C232.606 214.832 236.233 214.827 239.433 214.547C243.327 214.206 247.238 213.733 251.141 213.538C256.502 213.269 261.883 213.36 267.25 213.257C273.5 213.971 279.834 213.445 286.108 213.605C286.028 213.626 285.948 213.648 285.867 213.666C283.762 214.153 281.367 213.997 279.208 214.037C272.251 214.165 265.326 213.83 258.375 213.88C253.031 213.919 247.886 214.67 242.578 215.017C240.895 215.127 236.024 214.805 235.01 216.105C234.814 216.356 234.778 216.499 234.672 216.773C235.26 217.358 235.736 217.319 236.534 217.341C240.179 217.439 243.921 217.212 247.585 217.238C254.057 217.283 260.516 217.54 266.99 217.551C270.978 217.557 278.548 217.235 282.283 217.919C273.611 218.558 264.721 217.948 256.023 218.03C249.71 217.873 243.396 217.847 237.081 217.954C233.732 217.991 229.946 218.398 226.648 217.845Z" fill="#4F91E0"/>
<path d="M321 196.5C321.019 196.878 320.86 194.141 320.972 194.5C322.327 193.475 318.572 197.151 319.469 195.652C321.796 191.764 330.434 190.055 334.502 188.929C336.346 188.419 338.422 187.579 340.301 187.318C341.102 187.207 341.211 187.423 341.757 187.926C349.468 191.947 357.118 196.547 365.115 199.946C366.105 200.384 366.906 200.846 367.756 201.518C367.27 202.637 366.763 203.747 366.234 204.848C365.063 207.595 364.344 210.137 363.459 212.952L361.76 212.951C346.809 213.807 331.759 213.014 316.787 213.074L315.391 205.559C315.798 203.719 315.283 201.847 315.391 200.002C315.463 198.762 315.835 196.752 316 195.5C315.5 195 323.374 191.757 321 196.5Z" fill="#3158B7"/>
<path d="M291.045 187.739C293.054 186.88 299.704 189.793 301.963 190.683C310.556 194.07 305.5 191.5 312.5 194.5C314 195 315.54 193.995 316.5 195L316.787 213.074C306.586 212.996 296.304 213.296 286.108 213.605C279.834 213.445 273.5 213.971 267.25 213.257C267.293 213.167 267.345 213.08 267.378 212.986C268.103 210.948 263.421 201.839 262.414 199.405C264.851 198.458 267.39 197.808 269.836 196.808C273.044 195.497 286.703 188.347 288.963 188.533L289.5 188.5C290.249 188.144 290.627 188.446 291.045 187.739Z" fill="#15337C"/>
<path d="M316.158 217.961C333.626 218.066 351.372 216.648 368.745 218.663C369.093 229.235 368.662 239.88 368.76 250.467L387.46 250.44C390.609 250.437 393.982 250.193 397.101 250.535C392.229 251.156 386.936 250.764 382.02 250.742L316.217 250.777C316.034 247.101 316.2 243.334 316.205 239.649C316.23 232.42 316.215 225.19 316.158 217.961Z" fill="#5BBFFC"/>
<path d="M229.351 250.403C226.903 249.086 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C229.946 218.398 233.732 217.991 237.081 217.954C243.396 217.847 249.71 217.873 256.023 218.03C264.721 217.948 273.611 218.558 282.283 217.919L316.158 217.961C316.215 225.19 316.23 232.42 316.205 239.649C316.2 243.334 316.034 247.101 316.217 250.777L382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C234.444 250.731 231.884 250.523 229.351 250.403Z" fill="#3D7ECD"/>
<path d="M229.5 250.5C227.052 249.183 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C229.946 218.398 233.732 217.991 237.081 217.954C243.396 217.847 249.71 217.873 256.023 218.03C258.057 218.243 260.081 218.239 262.124 218.245C262.036 228.982 261.779 239.713 261.935 250.452C267.117 250.445 282.096 249.873 286.095 250.724C281.121 250.989 276.017 250.759 271.029 250.75C261.584 250.723 252.14 250.748 242.696 250.824C240.816 250.676 238.867 250.759 236.978 250.742C234.444 250.731 232.033 250.62 229.5 250.5Z" fill="#3158B7"/>
<path d="M242.696 250.824C252.14 250.748 261.584 250.723 271.029 250.75C276.017 250.759 281.121 250.989 286.095 250.724C296.126 250.796 306.188 250.976 316.217 250.777L382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C238.867 250.759 240.816 250.676 242.696 250.824Z" fill="url(#paint0_linear_4222_6997)"/>
<path d="M236.978 250.742C238.867 250.759 240.816 250.676 242.696 250.824C242.881 250.874 243.066 250.928 243.252 250.974C245.298 251.479 247.92 251.212 250.051 251.224C248.443 251.53 247.073 251.291 245.943 252.702C245.85 253.55 245.913 253.633 246.31 254.399C248.336 255.702 252.778 255.161 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742Z" fill="#15337C"/>
<path d="M315.839 263.948C315.537 262.501 315.708 261.013 315.229 259.629C314.46 258.966 313.976 259.049 312.984 258.933C313.102 258.884 313.213 258.814 313.338 258.787C315.769 258.246 325.519 258.983 328.801 258.983L357.499 259.017C359.803 259.006 366.156 258.505 368.097 258.96C368.485 259.051 368.546 259.119 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663C307.941 265.704 312.848 264.583 315.839 263.948Z" fill="url(#paint1_linear_4222_6997)"/>
<path d="M334.406 268.36C340.718 267.927 347.148 267.514 353.456 268.191C355.699 268.431 360.368 269.793 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965C338.013 279.917 337.843 278.75 337.605 277.665C336.155 274.684 336.233 271.392 334.406 268.36Z" fill="#4F91E0"/>
<path d="M337.605 277.665C338.101 277.244 338.105 277.263 338.759 277.253C340.026 277.925 341.077 278.732 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965C338.013 279.917 337.843 278.75 337.605 277.665Z" fill="#5BBFFC"/>
<path d="M162.994 506.672C163.51 506.136 164.063 505.686 164.643 505.225C167.325 506.032 178.659 505.485 182.142 505.49L221.176 505.555C226.755 505.582 232.376 505.443 237.949 505.628C269.102 506.183 300.329 505.731 331.491 505.742L372.011 505.764C380.199 505.77 388.457 505.962 396.635 505.618C396.442 517.711 396.022 529.884 396.433 541.975C409.395 542.082 422.357 542.091 435.32 542.002C441.062 542.005 447.41 541.469 453.076 542.23C457.801 542.254 462.724 542.135 467.419 542.679C465.179 543.78 463.234 548.572 462.043 550.796V551.298L462.794 551.648C454.827 551.784 446.872 552.172 438.929 552.813C436.511 552.995 434.06 552.553 431.644 552.413C428.22 552.214 424.779 552.237 421.352 552.25C402.933 552.322 384.531 552.526 366.11 552.346L227.563 552.274C215.642 552.277 203.672 552.344 191.757 552.13C182.178 551.958 172.491 551.389 162.923 551.866C164.053 549.046 163.345 544.905 162.658 542.024C162.018 536.512 162.277 530.737 162.288 525.186C162.299 519.081 161.931 512.699 162.994 506.672Z" fill="#15337C"/>
<path d="M162.994 506.672C163.51 506.136 164.063 505.686 164.643 505.225C167.325 506.032 178.659 505.485 182.142 505.49L221.176 505.555C226.755 505.582 232.376 505.443 237.949 505.628C237.694 517.722 237.219 529.932 237.571 542.021L396.433 541.975C409.395 542.082 422.357 542.091 435.32 542.002C441.062 542.005 447.41 541.469 453.076 542.23C445.432 542.447 437.752 542.287 430.104 542.288L384.066 542.238L236.646 542.27L181.968 542.237C175.536 542.231 169.083 541.914 162.658 542.024C162.018 536.512 162.277 530.737 162.288 525.186C162.299 519.081 161.931 512.699 162.994 506.672Z" fill="#3158B7"/>
<path d="M237.949 505.628C269.102 506.183 300.329 505.731 331.491 505.742L372.011 505.764C380.199 505.77 388.457 505.962 396.635 505.618C396.442 517.711 396.022 529.884 396.433 541.975L237.571 542.021C237.219 529.932 237.694 517.722 237.949 505.628Z" fill="#3D7ECD"/>
<path d="M316.173 450.214L316.356 450.171C327.599 451.012 338.68 453.515 349.747 455.526C360.085 457.318 370.442 458.993 380.817 460.552C388.881 461.984 396.892 463.653 404.942 465.147C409.11 465.92 413.349 466.406 417.485 467.324C418.03 466.869 417.89 466.842 417.96 466.175C418.868 471.139 419.01 474.417 418.882 479.505C424.212 480.991 428.904 484.71 433.752 487.295L467.07 504.914C467.988 505.502 468.749 505.944 468.955 507.111C469.514 510.282 469.469 539.486 468.739 541.824C468.292 542.267 468.021 542.487 467.419 542.679C462.724 542.135 457.801 542.254 453.076 542.23C447.41 541.469 441.062 542.005 435.32 542.002C422.357 542.091 409.395 542.082 396.433 541.975C396.022 529.884 396.442 517.711 396.635 505.618C388.457 505.962 380.199 505.77 372.011 505.764L331.491 505.742C300.329 505.731 269.102 506.183 237.949 505.628C232.376 505.443 226.755 505.582 221.176 505.555L182.142 505.49C178.659 505.485 167.325 506.032 164.643 505.225C166.684 503.613 169.31 502.543 171.632 501.378C176.276 499.048 181.007 496.831 185.604 494.419C192.121 490.999 198.566 487.372 204.999 483.795C207.005 482.679 208.919 481.328 210.961 480.297C211.664 479.943 212.437 479.729 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214Z" fill="#73E4FA"/>
<path d="M316.173 450.214L316.356 450.171C327.599 451.012 338.68 453.515 349.747 455.526C360.085 457.318 370.442 458.993 380.817 460.552C388.881 461.984 396.892 463.653 404.942 465.147C409.11 465.92 413.349 466.406 417.485 467.324C418.03 466.869 417.89 466.842 417.96 466.175C418.868 471.139 419.01 474.417 418.882 479.505C407.542 478.716 395.89 479.365 384.514 479.436C370.443 479.525 356.374 479.202 342.302 479.225L326.141 479.166C322.94 479.164 319.619 479.396 316.441 479.069C316.286 469.43 316.022 459.862 316.173 450.214Z" fill="#3D7ECD"/>
<path d="M467.07 504.914C467.988 505.502 468.749 505.944 468.955 507.111C469.514 510.282 469.469 539.486 468.739 541.824C468.292 542.267 468.021 542.487 467.419 542.679C462.724 542.135 457.801 542.254 453.076 542.23C447.41 541.469 441.062 542.005 435.32 542.002C422.357 542.091 409.395 542.082 396.433 541.975C396.022 529.884 396.442 517.711 396.635 505.618C399.774 505.933 403.063 505.785 406.221 505.786L422.299 505.784C435.31 505.785 448.476 506.228 461.464 505.68C463.339 505.6 465.314 505.634 467.07 504.914Z" fill="#5BBFFC"/>
<path d="M252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214C316.022 459.862 316.286 469.43 316.441 479.069C313.41 479.108 310.372 479.425 307.371 479.847C292.468 483.6 277.556 488.899 263.025 493.906C257.708 495.738 251.924 497.128 246.801 499.408C243.673 500.8 240.545 502.657 238.239 505.216L351.838 505.253L382.181 505.431C386.77 505.429 392.191 504.724 396.635 505.618C388.457 505.962 380.199 505.77 372.011 505.764L331.491 505.742C300.329 505.731 269.102 506.183 237.949 505.628C232.376 505.443 226.755 505.582 221.176 505.555L182.142 505.49C178.659 505.485 167.325 506.032 164.643 505.225C166.684 503.613 169.31 502.543 171.632 501.378C176.276 499.048 181.007 496.831 185.604 494.419C192.121 490.999 198.566 487.372 204.999 483.795C207.005 482.679 208.919 481.328 210.961 480.297C211.664 479.943 212.437 479.729 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143Z" fill="url(#paint2_linear_4222_6997)"/>
<path d="M252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214C316.022 459.862 316.286 469.43 316.441 479.069C313.41 479.108 310.372 479.425 307.371 479.847C280.73 478.905 253.933 479.206 227.271 479.298C222.578 479.314 217.879 479.538 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143Z" fill="#15337C"/>
<path d="M305.601 267.663C308.244 267.144 310.643 265.828 313.234 265.15C314 264.949 314.562 264.951 315.272 265.334C316.559 267.352 315.076 269.916 315.579 272.213C316.083 270.149 314.993 265.535 316.192 264.037L316.638 264.129C322.458 264.909 327.237 267.42 330.826 272.131C332.112 273.818 335.013 279.586 335.64 280.22C336.122 280.707 336.996 280.978 337.631 281.175L338.374 280.965L338.622 280.946C340.078 289.201 342.561 297.284 344.607 305.405C347.019 314.981 349.071 324.725 351.327 334.35L372.075 421.626C374.16 430.227 376.712 438.738 378.694 447.356C379.359 450.33 379.896 453.327 380.305 456.347C380.489 457.712 380.516 459.221 380.817 460.552C370.442 458.993 360.085 457.318 349.747 455.526C338.68 453.515 327.599 451.012 316.356 450.171L316.173 450.214C310.98 450.069 305.401 451.522 300.283 452.407L276.475 456.519C268.511 457.946 260.461 458.819 252.483 460.143C239.827 462.451 227.245 465.759 214.452 467.044C214.01 466.597 214.147 466.81 213.961 466.462C217.335 455.731 219.76 444.734 222.73 433.889L237.945 379.405L269.481 268.727C276.08 271.943 283.761 280.886 291.552 279.784C291.072 279.562 290.788 279.534 290.274 279.596C290.098 279.617 289.922 279.642 289.746 279.665L289.264 279.106L289.342 279.245C292.836 276.598 291.539 275.429 292.771 271.723C293.748 268.782 296.45 270.04 298.638 269.418C299.724 269.109 300.798 268.358 301.795 267.828C302.592 267.405 303.346 267.089 304.249 267.371C303.581 267.707 302.97 267.994 302.422 268.516L302.836 268.679C303.726 268.445 304.526 268.174 305.359 267.78L305.601 267.663Z" fill="#3D7ECD"/>
<path d="M213.961 466.462C217.335 455.731 219.76 444.734 222.73 433.889L237.945 379.405L269.481 268.727C276.08 271.943 283.761 280.886 291.552 279.784C294.318 279.786 297.526 280.103 300.239 279.671C302.32 279.34 293.35 281.165 295.5 281C292.765 282.129 294.815 282.321 291.94 282.461C291.8 282.468 291.66 282.481 291.519 282.489C291.435 282.494 291.35 282.496 291.266 282.5C290.329 287.67 289.473 292.882 288.349 298.015L265.21 399.097L256.243 437.602C255.364 441.302 250.844 457.078 251.4 459.744C251.766 459.902 252.097 460.037 252.483 460.143C239.827 462.451 227.245 465.759 214.452 467.044C214.01 466.597 214.147 466.81 213.961 466.462Z" fill="#3158B7"/>
<path d="M316.638 264.129C322.458 264.909 327.237 267.42 330.826 272.131C332.112 273.818 335.013 279.586 335.64 280.22C336.122 280.707 336.996 280.978 337.631 281.175L338.374 280.965L338.622 280.946C340.078 289.201 342.561 297.284 344.607 305.405C347.019 314.981 349.071 324.725 351.327 334.35L372.075 421.626C374.16 430.227 376.712 438.738 378.694 447.356C379.359 450.33 379.896 453.327 380.305 456.347C380.489 457.712 380.516 459.221 380.817 460.552C370.442 458.993 360.085 457.318 349.747 455.526C338.68 453.515 327.599 451.012 316.356 450.171L316.124 267.318C316.132 266.225 316.016 265.039 316.638 264.129Z" fill="#5BBFFC"/>
<path d="M162.602 542.012C222.681 542.667 282.903 542.133 343 542.146L421.144 542.172C436.934 542.179 452.86 542.406 468.631 542C468.259 556.284 468.631 576.5 466.168 585H164.5C162.602 574 162.11 556.297 162.602 542.012Z" fill="#2E57AB"/>
<path d="M263.193 261.724L235.532 253.517L232.5 252.5L229.5 250.5L402 250L377.5 261.724L364 275.5L339.134 284H289.345L269.731 271.69L263.193 261.724Z" fill="#2950B6"/>
<defs>
<linearGradient id="paint0_linear_4222_6997" x1="313.33" y1="251.065" x2="313.326" y2="279.202" gradientUnits="userSpaceOnUse">
<stop stop-color="#122869"/>
<stop offset="1" stop-color="#2950B6"/>
</linearGradient>
<linearGradient id="paint1_linear_4222_6997" x1="342.025" y1="256.698" x2="339.592" y2="280.614" gradientUnits="userSpaceOnUse">
<stop stop-color="#3052A7"/>
<stop offset="1" stop-color="#2671CE"/>
</linearGradient>
<linearGradient id="paint2_linear_4222_6997" x1="233.914" y1="479.018" x2="235.081" y2="504.885" gradientUnits="userSpaceOnUse">
<stop stop-color="#486DC9"/>
<stop offset="1" stop-color="#599BEA"/>
</linearGradient>
</defs>
<?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: 29 KiB

After

Width:  |  Height:  |  Size: 819 B

+21 -2
View File
@@ -33,6 +33,14 @@ function App() {
body: {
backgroundColor: theme.palette.background.default,
},
".Toastify__progress-bar": {
height: 2,
bottom: 0,
top: "auto",
},
".Toastify__progress-bar--bg": {
display: "none",
},
}}
/>
<AppLayout>
@@ -41,12 +49,23 @@ function App() {
<ToastContainer
newestOnTop={true}
theme={mode}
icon={false}
style={
{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
"--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>
@@ -5,7 +5,7 @@ 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;
@@ -32,11 +32,11 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25);
const latestChecks = checks.slice(-MAX_RECENT_CHECKS);
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
const placeholders = Array(25 - latestChecks.length).fill({
if (latestChecks.length !== MAX_RECENT_CHECKS) {
const placeholders = Array(MAX_RECENT_CHECKS - latestChecks.length).fill({
status: "placeholder" as const,
});
data = [...latestChecks, ...placeholders];
@@ -44,13 +44,13 @@ export const HeatmapResponseTime = ({
data = latestChecks;
}
return (
<Box sx={{ width: "100%" }}>
<Box width={"100%"}>
<Box
width={"100%"}
display={"grid"}
gap={gap}
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: gap,
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "stretch",
}}
>
@@ -82,50 +82,50 @@ export const HeatmapResponseTime = ({
check={check}
>
<Box
display={"grid"}
sx={{
display: "grid",
gridTemplateRows: "auto auto",
}}
>
<Box
display={"flex"}
gap={theme.spacing(2)}
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(2),
}}
>
<Box
width={"100%"}
sx={{
position: "relative",
width: "100%",
aspectRatio: "10",
}}
>
<Box
bgcolor={statusBg}
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
bgcolor: statusBg,
borderRadius: theme.spacing(0.5),
...availabilityCellSx,
}}
/>
<Box
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
pointerEvents: "none",
borderRadius: theme.spacing(0.5),
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
}}
/>
</Box>
<Box
width={"100%"}
bgcolor={respBg}
borderRadius={theme.shape.borderRadius}
sx={{
width: "100%",
aspectRatio: "1 / 1",
bgcolor: respBg,
borderRadius: theme.spacing(0.5),
...responseCellSx,
}}
/>
@@ -7,6 +7,7 @@ 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";
interface HistogramResponseTimeProps {
checks: CheckSnapshot[];
@@ -34,7 +35,7 @@ const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats
}
const responseTimes = validChecks.map((check) => check.originalResponseTime);
const max = Math.max(...responseTimes);
const max = Math.round(Math.max(...responseTimes));
const avg = Math.round(
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
);
@@ -57,8 +58,11 @@ export const HistogramResponseTime = ({
if (!Array.isArray(checks) || checks.length === 0) return null;
const data =
checks.length !== 25
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
checks.length !== MAX_RECENT_CHECKS
? [
...checks,
...Array(MAX_RECENT_CHECKS - checks.length).fill({ status: "placeholder" }),
]
: checks;
const chartHeight = typeof height === "number" ? `${height}px` : height;
@@ -69,14 +73,20 @@ export const HistogramResponseTime = ({
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
minWidth={70}
width={110}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
>
<Typography variant="body2">
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography variant="body2">
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
@@ -86,17 +96,17 @@ export const HistogramResponseTime = ({
<Stack
direction={statsPosition === "left" ? "row-reverse" : "row"}
alignItems="center"
sx={{ width: "100%" }}
width={"100%"}
>
<Box sx={{ flex: 1 }}>
<Box
width={"100%"}
display={"grid"}
gap={gridGap}
height={chartHeight}
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: gridGap,
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "end",
height: chartHeight,
}}
>
{data.map((check, index) => {
@@ -108,23 +118,23 @@ export const HistogramResponseTime = ({
: theme.palette.error.light;
const bar = (
<Box
width={"100%"}
height={"100%"}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
position: "relative",
width: "100%",
height: "100%",
borderRadius: theme.spacing(1),
bgcolor: theme.palette.action.hover,
overflow: "hidden",
}}
>
<Box
width={"100%"}
height={isPlaceholder ? 0 : heightPct}
bgcolor={barColor}
sx={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
height: isPlaceholder ? 0 : heightPct,
bgcolor: barColor,
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
@@ -133,8 +143,8 @@ export const HistogramResponseTime = ({
return isPlaceholder ? (
<Box
height={"100%"}
key={index}
sx={{ height: "100%" }}
>
{bar}
</Box>
@@ -8,7 +8,7 @@ import type { ResponsiveStyleValue } from "@mui/system";
import { useTheme } from "@mui/material/styles";
type BaseChartProps = React.PropsWithChildren<{
icon: React.ReactNode;
icon?: React.ReactNode;
title: string;
width?: number | string;
maxWidth?: number | string;
@@ -17,7 +17,6 @@ type BaseChartProps = React.PropsWithChildren<{
export const BaseChart = ({
children,
icon,
title,
width = "100%",
maxWidth = "100%",
@@ -38,34 +37,12 @@ export const BaseChart = ({
gap={theme.spacing(LAYOUT.MD)}
flex={1}
>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(LAYOUT.XS)}
<Typography
variant="eyebrow"
color="text.secondary"
>
{icon && (
<BaseBox
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
backgroundColor: theme.palette.action.hover,
"& svg": {
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.secondary,
},
},
}}
>
{icon}
</BaseBox>
)}
<Typography variant="h2">{title}</Typography>
</Stack>
{title}
</Typography>
<Box flex={1}>{children}</Box>
</Stack>
</BaseBox>
@@ -1,18 +1,19 @@
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import { LAYOUT } from "@/Utils/Theme/constants";
import {
ErrorFallback,
EmptyFallback,
EmptyMonitorFallback,
BaseFallback,
} 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 { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
import { LanguageSelector, SwitchTheme } from "@/Components/inputs";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
@@ -21,16 +22,15 @@ import { Link as RouterLink } from "react-router-dom";
import { Typography } from "@mui/material";
export const PageSpeedKeyPriorityFallback = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<BaseFallback>
<Alert
severity="warning"
sx={{
width: "100%",
maxWidth: 600,
}}
>
<Typography>
<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={{
@@ -38,15 +38,18 @@ export const PageSpeedKeyPriorityFallback = () => {
<Link
component={RouterLink}
to="/settings"
color="inherit"
fontWeight="inherit"
color={theme.palette.primary.main}
fontWeight={600}
sx={{
textDecoration: "underline",
}}
/>
),
}}
/>
</Typography>
</Alert>
</BaseFallback>
</NoticeBanner>
}
/>
);
};
@@ -55,6 +58,9 @@ interface BasePageProps extends StackProps {
error?: boolean;
children: React.ReactNode;
breadcrumbOverride?: string[];
headerKey?: string;
backTo?: string;
backLabel?: string;
}
export const BasePage = ({
@@ -62,9 +68,13 @@ export const BasePage = ({
error,
children,
breadcrumbOverride,
headerKey,
backTo,
backLabel,
...props
}: BasePageProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (loading) {
return (
@@ -95,7 +105,16 @@ export const BasePage = ({
spacing={theme.spacing(LAYOUT.LG)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
{headerKey ? (
<PageHeader
title={t(`pages.${headerKey}.header.title`)}
description={t(`pages.${headerKey}.header.description`)}
backTo={backTo}
backLabel={backLabel}
/>
) : (
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
)}
{children}
</Stack>
);
@@ -105,11 +124,12 @@ interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
totalCount: number;
bullets: string[] | unknown;
description?: string;
page: string;
actionButtonText: string;
actionLink: string;
children: React.ReactNode;
headerKey?: string;
}
export const BasePageWithStates = ({
@@ -117,10 +137,11 @@ export const BasePageWithStates = ({
error,
totalCount,
page,
bullets,
description,
actionButtonText,
actionLink,
children,
headerKey,
...props
}: BasePageWithStatesProps) => {
const showLoading = loading && totalCount === 0;
@@ -128,7 +149,7 @@ export const BasePageWithStates = ({
if (!loading && totalCount === 0) {
return (
<EmptyFallback
bullets={bullets}
description={description}
title={page}
actionButtonText={actionButtonText}
actionLink={actionLink}
@@ -140,6 +161,7 @@ export const BasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -155,6 +177,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
headerKey?: string;
}
export const MonitorBasePageWithStates = ({
@@ -165,6 +188,7 @@ export const MonitorBasePageWithStates = ({
actionLink,
children,
priorityFallback,
headerKey,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
@@ -172,15 +196,7 @@ export const MonitorBasePageWithStates = ({
const showLoading = loading && totalCount === 0;
if (priorityFallback) {
return (
<BasePage
loading={loading}
error={error}
{...props}
>
<Stack height={"100%"}>{priorityFallback}</Stack>
</BasePage>
);
return <>{priorityFallback}</>;
}
if (!loading && totalCount === 0) {
@@ -188,7 +204,7 @@ export const MonitorBasePageWithStates = ({
<EmptyMonitorFallback
page={page}
title={t(`pages.${page}.fallback.title`)}
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
description={t(`pages.${page}.fallback.description`)}
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
@@ -199,6 +215,7 @@ export const MonitorBasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -215,51 +232,163 @@ export const BaseAuthPage = ({
title,
subtitle,
children,
...props
component,
onSubmit,
}: BaseAuthPageProps) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<BasePage
breadcrumbOverride={[]}
gap={theme.spacing(LAYOUT.MD)}
alignItems={"center"}
justifyContent={"center"}
<Stack
direction={{ xs: "column", md: "row" }}
minHeight="100vh"
position={"relative"}
{...props}
width="100%"
position="relative"
sx={{ backgroundColor: theme.palette.background.default }}
>
<HeaderAuthControls
hideLogo
py={theme.spacing(LAYOUT.XS)}
position={"absolute"}
top={0}
left={0}
/>
<Box width={{ xs: 60, sm: 70, md: 80 }}>
<Logo
width={"100%"}
height={"100%"}
/>
</Box>
<Stack alignItems={"center"}>
<Typography
variant="h1"
mb={theme.spacing(SPACING.LG)}
<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,
}}
>
{title}
</Typography>
<Typography variant="h2">{subtitle}</Typography>
<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
gap={theme.spacing(LAYOUT.MD)}
width={{
xs: "80%",
md: "25%",
lg: "15%",
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",
},
}}
>
{children}
<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>
</BasePage>
</Stack>
);
};
@@ -0,0 +1,158 @@
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>
);
};
@@ -0,0 +1,197 @@
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,12 +1,8 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
import { Button } from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useNavigate } from "react-router";
import useMediaQuery from "@mui/material/useMediaQuery";
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 {
@@ -15,30 +11,13 @@ interface BaseFallbackProps extends StackProps {
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Stack
alignItems={"center"}
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "10%"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }}
bgcolor={theme.palette.background.paper}
border={1}
borderColor={theme.palette.divider}
borderRadius={theme.shape.borderRadius}
sx={{
borderStyle: "dashed",
}}
alignItems="center"
justifyContent="center"
sx={{ width: "100%", py: theme.spacing(LAYOUT.LG) }}
{...props}
>
<SkeletonCard showHalo={true} />
{children}
</Stack>
);
@@ -51,126 +30,56 @@ export const ErrorFallback = ({
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(LAYOUT.XS)}
color={theme.palette.text.secondary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={subtitle}
/>
);
};
export const EmptyFallback = ({
title,
bullets,
description,
actionButtonText,
actionLink,
}: {
title: string;
bullets: any;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(LAYOUT.MD)}
zIndex={1}
alignItems="center"
>
<Typography component="h1">{title}</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(SPACING.MD),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string) => (
<BulletPointCheck
text={bullet}
key={`${bullet}-${Math.random()}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
export const EmptyMonitorFallback = ({
page,
title,
bullets,
description,
actionButtonText,
actionLink,
}: {
page: string;
title: string;
bullets: any;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(LAYOUT.MD)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(SPACING.MD),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
+24 -14
View File
@@ -1,30 +1,36 @@
import { BaseChart } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Box, { type BoxProps } from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useMemo, useState, useEffect } from "react";
import { forwardRef, useMemo, useState, useEffect } from "react";
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
export const Gauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
precision = 1,
unit = "%",
}: {
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));
@@ -51,17 +57,19 @@ export const Gauge = ({
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
if (isLoading) {
return;
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}`}
@@ -105,7 +113,7 @@ export const Gauge = ({
</Typography>
</Box>
);
};
});
export const DetailGauge = ({
title,
@@ -114,6 +122,7 @@ export const DetailGauge = ({
upperValue,
lowerLabel,
lowerValue,
maxWidth = 225,
}: {
title: string;
progress: number;
@@ -121,13 +130,14 @@ export const DetailGauge = ({
upperValue?: string | number;
lowerLabel?: string;
lowerValue?: string | number;
maxWidth?: number;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={null}
title={title}
maxWidth={225}
maxWidth={maxWidth}
>
<Stack
alignItems={"center"}
@@ -1,12 +1,13 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/design-elements";
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -17,7 +18,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
return (
<Stack>
<Typography
fontSize={typographyLevels.xl}
fontSize={typographyLevels.xxl}
fontWeight={500}
overflow={"hidden"}
textOverflow={"ellipsis"}
@@ -28,19 +29,25 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
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 />
@@ -0,0 +1,55 @@
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>
);
};
@@ -0,0 +1,164 @@
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 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: 640,
}}
>
{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: 32,
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>
);
};
@@ -53,7 +53,7 @@ export const ConfigBox = ({
rightContent,
}: {
title: string;
subtitle: string;
subtitle: React.ReactNode;
leftContent?: React.ReactNode;
rightContent: React.ReactNode;
}) => {
@@ -63,9 +63,9 @@ export const ConfigBox = ({
left={
<Stack spacing={theme.spacing(LAYOUT.XS)}>
<Typography
textTransform={"capitalize"}
component="h2"
variant="h2"
variant="eyebrow"
color="text.secondary"
>
{title}
</Typography>
@@ -69,7 +69,12 @@ export const StatBox = ({
>
<Stack onClick={onClick}>
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
<Typography color={textColor}>{title}</Typography>
<Typography
variant="eyebrow"
color={palette ? textColor : "text.secondary"}
>
{title}
</Typography>
{tooltip && (
<TooltipWithInfo
title={tooltip}
@@ -21,7 +21,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
overflow: "hidden",
position: "relative",
flex: 1,
padding: theme.spacing(4),
padding: theme.spacing(6),
...sx,
}}
>
@@ -65,9 +65,13 @@ const StatusBox = ({
<BGBox sx={sx}>
<Stack spacing={theme.spacing(4)}>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.text.secondary}
sx={{
fontSize: 11,
fontWeight: 500,
letterSpacing: "0.08em",
}}
>
{label}
</Typography>
@@ -0,0 +1,40 @@
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>
);
};
@@ -93,3 +93,20 @@ export const ValueLabel = ({ value, text }: { value: ValueType; text: string })
</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>
);
};
@@ -0,0 +1,42 @@
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>
);
};
@@ -0,0 +1,32 @@
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>
);
};
+39 -45
View File
@@ -18,10 +18,9 @@ import {
ChevronsRight,
ChevronLeft,
ChevronRight,
Coffee,
Ellipsis,
PartyPopper,
} from "lucide-react";
import { EmptyState } from "./EmptyState";
import TablePagination from "@mui/material/TablePagination";
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
@@ -36,6 +35,9 @@ 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;
};
@@ -110,6 +112,24 @@ export function DataTable<
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
@@ -124,7 +144,7 @@ export function DataTable<
component="div"
color={theme.palette.text.primary}
>
{header.content}
{header.mobileLabel ?? header.content}
</Typography>
</Grid2>
<Grid2
@@ -163,14 +183,19 @@ export function DataTable<
},
"& .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: 500,
textTransform: "uppercase",
letterSpacing: "0.08em",
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
fontSize: theme.typography.fontSize,
fontSize: 11,
},
"& :is(td)": {
backgroundColor: theme.palette.background.paper,
@@ -189,7 +214,7 @@ export function DataTable<
{headers.map((header, idx) => {
return (
<TableCell
align={idx === 0 ? "left" : "center"}
align={header.align ?? (idx === 0 ? "left" : "center")}
key={header.id}
>
{header.content}
@@ -218,7 +243,7 @@ export function DataTable<
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
align={header.align ?? (index === 0 ? "left" : "center")}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
@@ -548,49 +573,18 @@ export const Pagination = ({ ...props }: PaginationProps) => {
);
};
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
const theme = useTheme();
const EmptyView = ({ text }: { text?: string; positive?: boolean }) => {
const { t } = useTranslation();
const Icon = positive ? PartyPopper : Coffee;
const theme = useTheme();
return (
<Stack
alignItems={"center"}
justifyContent={"center"}
<Box
sx={{
py: theme.spacing(LAYOUT.XL),
px: theme.spacing(LAYOUT.XS),
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
textAlign: "center",
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper,
}}
>
<Box
sx={{
width: 64,
height: 64,
borderRadius: "50%",
backgroundColor: theme.palette.action.hover,
display: "flex",
alignItems: "center",
justifyContent: "center",
mb: theme.spacing(SPACING.LG),
}}
>
<Icon
size={28}
strokeWidth={1}
color={theme.palette.text.secondary}
/>
</Box>
<Typography
variant="subtitle1"
color={theme.palette.text.primary}
sx={{ fontWeight: 500 }}
>
{text ?? t("common.table.empty")}
</Typography>
</Stack>
<EmptyState title={text ?? t("common.table.empty")} />
</Box>
);
};
@@ -6,7 +6,11 @@ import type { TooltipProps } from "@mui/material/Tooltip";
type StyledTooltipProps = TooltipProps;
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
export const Tooltip = ({
placement = "top",
slotProps,
...props
}: StyledTooltipProps) => {
const theme = useTheme();
return (
@@ -16,18 +20,18 @@ export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) =>
slotProps={{
tooltip: {
sx: {
background:
backgroundColor:
theme.palette.mode === "dark"
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
backgroundColor: "transparent",
color: "#ffffff",
? 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: "0 8px 24px rgba(0, 0, 0, 0.4)",
boxShadow: theme.shadows[6],
},
},
...slotProps,
}}
>
{props.children}
@@ -1,5 +1,8 @@
export * from "./Breadcrumb";
export * from "./PageHeader";
export * from "./BasePage";
export * from "./EmptyState";
export * from "./EmptyStateIllustration";
export * from "./Fallback";
export * from "./SkeletonCard";
export * from "./BulletPointCheck";
@@ -19,5 +22,9 @@ 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";
+25 -19
View File
@@ -14,17 +14,19 @@ type AutoCompleteInputProps = Omit<
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
fieldLabel?: string;
required?: boolean;
renderOptionContent?: (option: any) => React.ReactNode;
};
export const AutoCompleteInput = ({
fieldLabel,
required,
renderInput,
renderOptionContent,
...props
}: AutoCompleteInputProps) => {
const theme = useTheme();
const multiple = props.multiple;
const { renderOption: customRenderOption, ...autocompleteProps } = props;
const defaultRenderInput = (params: any) => (
<TextField
{...params}
@@ -34,7 +36,7 @@ export const AutoCompleteInput = ({
const autocomplete = (
<Autocomplete
{...props}
{...autocompleteProps}
disableCloseOnSelect={!!multiple}
popupIcon={
<ChevronDown
@@ -46,24 +48,28 @@ export const AutoCompleteInput = ({
renderInput={renderInput || defaultRenderInput}
getOptionKey={(option) => option.id}
renderTags={() => null}
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
renderOption={
customRenderOption ||
((props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
{multiple && <Checkbox checked={selected} />}
{option.name}
</Stack>
</ListItem>
);
}}
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
width="100%"
>
{multiple && <Checkbox checked={selected} />}
{renderOptionContent ? renderOptionContent(option) : option.name}
</Stack>
</ListItem>
);
})
}
sx={{
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
+31 -9
View File
@@ -1,19 +1,43 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
import { useTheme } from "@mui/material/styles";
import { useTheme, darken } from "@mui/material/styles";
import { HOVER } from "@/Utils/Theme/constants";
const PALETTE_COLORS = [
"primary",
"secondary",
"error",
"warning",
"info",
"success",
] as const;
type PaletteColor = (typeof PALETTE_COLORS)[number];
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
const theme = useTheme();
const outlinedSx =
const hoverOverrides: Record<string, unknown> = { boxShadow: "none" };
if (props.variant === "outlined") {
hoverOverrides.borderColor = theme.palette.text.secondary;
}
if (props.variant === "contained") {
const colorKey = (props.color ?? "primary") as PaletteColor;
const palette = theme.palette[colorKey];
if (palette && typeof palette === "object" && "main" in palette) {
hoverOverrides.backgroundColor = darken(palette.main, HOVER.DARKEN);
}
}
const variantSx =
props.variant === "outlined"
? {
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
"&:hover": {
borderColor: theme.palette.text.secondary,
},
}
: {};
return (
<Button
{...props}
@@ -26,10 +50,8 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
"&:hover": {
boxShadow: "none",
},
...outlinedSx,
"&:hover": hoverOverrides,
...variantSx,
...sx,
}}
/>
+8 -2
View File
@@ -1,7 +1,7 @@
import { forwardRef } from "react";
import Checkbox from "@mui/material/Checkbox";
import type { CheckboxProps } from "@mui/material/Checkbox";
import { Square, SquareCheck } from "lucide-react";
import { Square, SquareCheck, SquareMinus } from "lucide-react";
import { useTheme } from "@mui/material/styles";
type CheckboxInputProps = CheckboxProps & {
@@ -34,9 +34,15 @@ export const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(
strokeWidth={1.5}
/>
}
indeterminateIcon={
<SquareMinus
size={16}
strokeWidth={1.5}
/>
}
sx={{
color: theme.palette.text.secondary,
"&.Mui-checked": {
"&.Mui-checked, &.MuiCheckbox-indeterminate": {
color: theme.palette.primary.main,
},
"&:hover": { backgroundColor: "transparent" },
+48 -12
View File
@@ -2,10 +2,12 @@ import Dialog from "@mui/material/Dialog";
import type { DialogProps } from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { Button } from "@/Components/inputs";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { ReactNode } from "react";
@@ -16,7 +18,6 @@ export const DialogInput = ({
onConfirm,
onCancel,
confirmColor = "primary",
cancelColor = "error",
loading = false,
cancelText,
confirmText,
@@ -31,7 +32,6 @@ export const DialogInput = ({
onConfirm?(item: any): any;
onCancel?(item: any): any;
confirmColor?: "error" | "primary";
cancelColor?: "error" | "primary";
loading?: boolean;
cancelText?: string;
confirmText?: string;
@@ -41,22 +41,58 @@ export const DialogInput = ({
additionalButtons?: ReactNode;
}) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<Dialog
open={open}
maxWidth={maxWidth}
fullWidth={fullWidth}
onClose={(_event, reason) => {
if (reason !== "backdropClick" && onCancel) onCancel(undefined);
}}
>
{title && <DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>}
<DialogContent>
{content && <DialogContentText>{content}</DialogContentText>}
{children}
</DialogContent>
<DialogActions>
{title && (
<DialogTitle
pb={theme.spacing(LAYOUT.XS)}
bgcolor={theme.palette.action.hover}
borderBottom={`1px solid ${theme.palette.divider}`}
>
<Typography
component="span"
variant="h2"
fontWeight={600}
display={"block"}
>
{title}
</Typography>
{content && (
<Typography
variant="body1"
color={theme.palette.text.secondary}
mt={theme.spacing(LAYOUT.XS)}
display={"block"}
>
{content}
</Typography>
)}
</DialogTitle>
)}
{children && (
<DialogContent sx={{ pt: theme.spacing(LAYOUT.MD) }}>
<Box pt={theme.spacing(LAYOUT.XS)}>{children}</Box>
</DialogContent>
)}
<DialogActions
sx={{
p: theme.spacing(LAYOUT.MD),
pt: theme.spacing(LAYOUT.SM),
backgroundColor: theme.palette.action.hover,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<Button
loading={loading}
variant="contained"
color={cancelColor}
variant="outlined"
onClick={onCancel}
>
{cancelText ?? t("common.buttons.cancel")}
+1 -1
View File
@@ -15,9 +15,9 @@ export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelPr
return (
<Typography
component="label"
variant="body1"
htmlFor={htmlFor}
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.text.secondary,
marginBottom: theme.spacing(2),
+6 -1
View File
@@ -126,7 +126,12 @@ export const ImageUpload = ({
onChange={(e) => handleFile(e.target.files?.[0])}
style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
/>
<Upload size={24} />
<Upload
size={20}
strokeWidth={1.5}
color="currentColor"
style={{ opacity: 0.7 }}
/>
<Typography
variant="body2"
color="text.secondary"
@@ -1,26 +1,34 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/Components/inputs";
import "flag-icons/css/flag-icons.min.css";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "@/Features/UI/uiSlice";
import type { RootState } from "@/Types/state";
const langMap: Record<string, string> = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
export const languageNames: Record<string, string> = {
en: "English",
de: "Deutsch",
ca: "Català",
es: "Español",
fr: "Français",
"pt-BR": "Português (BR)",
cs: "Čeština",
fi: "Suomi",
tr: "Türkçe",
ru: "Русский",
uk: "Українська",
ar: "العربية",
th: "ไทย",
vi: "Tiếng Việt",
ja: "日本語",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
};
export const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const { language = "en" } = useSelector((state: RootState) => state.ui);
const dispatch = useDispatch();
const handleChange = (event: any) => {
@@ -35,64 +43,15 @@ export const LanguageSelector = () => {
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>
);
})}
{languages.map((lang) => (
<MenuItem
key={lang}
value={lang}
>
{languageNames[lang] ?? lang}
</MenuItem>
))}
</Select>
);
};
+1 -41
View File
@@ -1,52 +1,13 @@
import Radio from "@mui/material/Radio";
import type { RadioProps } from "@mui/material/Radio";
import { useTheme } from "@mui/material/styles";
import { Circle, CircleDot } from "lucide-react";
import FormControlLabel from "@mui/material/FormControlLabel";
import Typography from "@mui/material/Typography";
interface RadioInputProps extends RadioProps {}
export const RadioInput = ({ ...props }: RadioInputProps) => {
const theme = useTheme();
return (
<Radio
{...props}
icon={
<Circle
size={16}
strokeWidth={1.5}
/>
}
checkedIcon={
<CircleDot
size={14}
strokeWidth={1.5}
/>
}
sx={{
padding: 0,
mt: theme.spacing(0.5),
color: theme.palette.text.secondary,
"&.Mui-checked": {
color: theme.palette.primary.main,
"& svg circle": {
fill: theme.palette.primary.main,
},
},
"& .MuiSvgIcon-root": {
fontSize: 16,
},
"& svg": {
stroke: "currentColor",
},
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: "currentColor",
fill: "none",
},
}}
/>
);
return <Radio {...props} />;
};
export const RadioWithDescription = ({
@@ -79,7 +40,6 @@ export const RadioWithDescription = ({
backgroundColor: theme.palette.background.paper,
},
"& .MuiButtonBase-root": {
p: 0,
mr: theme.spacing(6),
},
}}
+2 -3
View File
@@ -11,15 +11,14 @@ interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
fieldLabel?: string;
required?: boolean;
placeholder?: string;
placeholderColor?: string;
}
const SelectInputInner = <T,>(
{ fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps<T>,
{ fieldLabel, required, placeholder, ...props }: SelectInputProps<T>,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const theme = useTheme();
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
const emptyPlaceholderColor = theme.palette.text.secondary;
const renderValue = (selected: unknown) => {
const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
+9 -17
View File
@@ -21,24 +21,16 @@ export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps
},
}}
sx={[
{
"& .MuiSwitch-switchBase": {
"&.Mui-checked": {
color: "#E0E0E0",
"& + .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
border: 0,
...(dualOption
? [
{
"& .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
},
},
},
},
...(dualOption && {
"& .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
},
}),
},
]
: []),
...additionalSx,
]}
/>
+35 -20
View File
@@ -5,6 +5,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { LAYOUT } from "@/Utils/Theme/constants";
interface TextInputProps extends Omit<TextFieldProps, "label"> {
fieldLabel?: string;
@@ -12,39 +13,53 @@ interface TextInputProps extends Omit<TextFieldProps, "label"> {
}
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
{ fieldLabel, required, ...props },
{ fieldLabel, required, sx, ...props },
ref
) {
const theme = useTheme();
const innerSx = {
width: "100%",
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: props.multiline ? "auto" : 34,
fontSize: typographyLevels.base,
overflow: "hidden",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"& .MuiFormHelperText-root": {
marginLeft: 0,
marginRight: 0,
marginTop: theme.spacing(1),
},
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
{
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
caretColor: theme.palette.text.primary,
transition: "background-color 5000s ease-in-out 0s",
},
};
const input = (
<TextField
{...props}
inputRef={ref}
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: 34,
fontSize: typographyLevels.base,
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"& .MuiFormHelperText-root": {
marginLeft: 0,
marginRight: 0,
marginTop: theme.spacing(1),
},
}}
sx={fieldLabel ? innerSx : { ...innerSx, ...sx }}
/>
);
if (fieldLabel) {
return (
<Stack spacing={theme.spacing(2)}>
<Stack
spacing={theme.spacing(LAYOUT.XXS)}
sx={sx}
>
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
{input}
</Stack>
+19 -1
View File
@@ -3,8 +3,17 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
import { useTheme } from "@mui/material/styles";
import { alpha } from "@mui/material/styles";
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const selectedBg = isDark
? alpha(theme.palette.primary.main, 0.22)
: alpha(theme.palette.primary.main, 0.06);
const selectedHoverBg = isDark
? alpha(theme.palette.primary.main, 0.3)
: alpha(theme.palette.primary.main, 0.1);
return (
<ToggleButton
{...props}
@@ -17,8 +26,18 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
color: theme.palette.text.secondary,
"&:hover": {
boxShadow: "none",
backgroundColor: theme.palette.action.hover,
},
"&.Mui-selected": {
backgroundColor: selectedBg,
color: theme.palette.text.primary,
fontWeight: 500,
"&:hover": {
backgroundColor: selectedHoverBg,
},
},
...sx,
}}
@@ -34,7 +53,6 @@ export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps)
sx={{
"& .MuiToggleButtonGroup-grouped": {
borderColor: theme.palette.divider,
borderRadius: 0,
"&:first-of-type": {
borderTopLeftRadius: 2,
@@ -0,0 +1,102 @@
import React from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Slide from "@mui/material/Slide";
import IconButton from "@mui/material/IconButton";
import { X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar";
interface BulkActionsBarProps {
selectedCount: number;
onCancel: () => void;
children?: React.ReactNode;
}
export const BulkActionsBar = ({
selectedCount,
onCancel,
children,
}: BulkActionsBarProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isOpen = selectedCount > 0;
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { width, collapsedWidth, transition } = useSidebar();
return (
<Box
sx={{
position: "fixed",
bottom: theme.spacing(LAYOUT.XS),
left: isSmall ? collapsedWidth : width,
right: 0,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
zIndex: theme.zIndex.snackbar,
transition: transition.replace("width", "left"),
}}
>
<Slide
direction="up"
in={isOpen}
mountOnEnter
unmountOnExit
>
<Paper
elevation={6}
sx={{
pointerEvents: "auto",
px: LAYOUT.MD,
py: LAYOUT.XS,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
gap: LAYOUT.XS,
boxShadow: theme.shadows[8],
}}
>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
<Typography
variant="body1"
fontWeight={600}
color={theme.palette.text.primary}
>
{t("pages.common.monitors.actions.bulkSelected", {
count: selectedCount,
})}
</Typography>
<IconButton
size="small"
onClick={onCancel}
aria-label={t("common.buttons.cancel")}
>
<X size={18} />
</IconButton>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
{children}
</Stack>
</Paper>
</Slide>
</Box>
);
};
@@ -1,9 +1,11 @@
import Stack from "@mui/material/Stack";
import { Select, Button } from "@/Components/inputs";
import { Select, Autocomplete, Button, TextField } from "@/Components/inputs";
import { ColoredLabel } from "@/Components/design-elements";
import MenuItem from "@mui/material/MenuItem";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { MonitorType } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import { Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
@@ -28,22 +30,34 @@ export const ControlsFilter = ({
setSelectedStatus,
selectedState,
setSelectedState,
tagOptions,
selectedTags,
setSelectedTags,
onClearFilters,
}: {
showTypes?: boolean;
selectedTypes?: MonitorType[];
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
selectedStatus: string;
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
selectedState: string;
setSelectedState: React.Dispatch<React.SetStateAction<string>>;
selectedStatus?: string;
setSelectedStatus?: React.Dispatch<React.SetStateAction<string>>;
selectedState?: string;
setSelectedState?: React.Dispatch<React.SetStateAction<string>>;
tagOptions?: Tag[];
selectedTags?: string[];
setSelectedTags?: React.Dispatch<React.SetStateAction<string[]>>;
onClearFilters: () => void;
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isFilterActive =
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
(selectedTypes?.length ?? 0) > 0 ||
Boolean(selectedStatus) ||
Boolean(selectedState) ||
(selectedTags?.length ?? 0) > 0;
const selectedTagObjects = (tagOptions ?? []).filter((tag) =>
(selectedTags ?? []).includes(tag.id)
);
return (
<Stack
direction={isSmall ? "column" : "row"}
@@ -66,34 +80,68 @@ export const ControlsFilter = ({
))}
</Select>
)}
<Select
placeholder="Status"
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
>
{statuses.map((status) => (
<MenuItem
key={status}
value={status}
>
<Typography textTransform={"capitalize"}>{status}</Typography>
</MenuItem>
))}
</Select>
<Select
placeholder="State"
value={selectedState}
onChange={(e) => setSelectedState(e.target.value)}
>
{states.map((state) => (
<MenuItem
key={state}
value={state}
>
<Typography textTransform={"capitalize"}>{state}</Typography>
</MenuItem>
))}
</Select>
{setSelectedStatus && (
<Select
placeholder="Status"
value={selectedStatus ?? ""}
onChange={(e) => setSelectedStatus(e.target.value)}
>
{statuses.map((status) => (
<MenuItem
key={status}
value={status}
>
<Typography textTransform={"capitalize"}>{status}</Typography>
</MenuItem>
))}
</Select>
)}
{setSelectedState && (
<Select
placeholder="State"
value={selectedState ?? ""}
onChange={(e) => setSelectedState(e.target.value)}
>
{states.map((state) => (
<MenuItem
key={state}
value={state}
>
<Typography textTransform={"capitalize"}>{state}</Typography>
</MenuItem>
))}
</Select>
)}
{tagOptions && setSelectedTags && (
<Autocomplete
multiple
options={tagOptions}
value={selectedTagObjects}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: Tag[]) => {
setSelectedTags(newValue.map((tag) => tag.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderOptionContent={(option) => (
<ColoredLabel
text={option.name}
color={option.color}
/>
)}
renderInput={(params) => (
<TextField
{...params}
placeholder={t("common.table.headers.tags")}
/>
)}
slotProps={{
popper: {
sx: { width: "fit-content !important", minWidth: 280 },
},
}}
sx={{ minWidth: 180 }}
/>
)}
{isFilterActive && (
<Button
variant="contained"
@@ -3,6 +3,8 @@ import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
import Map, { Marker, Popup } from "react-map-gl/maplibre";
import type { MapRef } from "react-map-gl/maplibre";
import type { FlatGeoCheck } from "@/Types/GeoCheck";
import { useTranslation } from "react-i18next";
import { formatStatusCode } from "@/Utils/statusCode";
import "maplibre-gl/dist/maplibre-gl.css";
interface GeoChecksMapProps {
@@ -14,6 +16,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
const { t } = useTranslation();
const mapPopupStyles = (
<GlobalStyles
@@ -174,7 +177,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
>
Status Code:
</Typography>{" "}
{selectedCheck.statusCode}
{formatStatusCode(selectedCheck.statusCode, t)}
</Typography>
<Typography variant="body2">
<Typography
@@ -9,6 +9,8 @@ import {
ResponsiveContainer,
Text,
} from "recharts";
import type { TooltipProps } from "recharts";
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
import Typography from "@mui/material/Typography";
import { useSelector } from "react-redux";
@@ -18,13 +20,14 @@ import {
tooltipDateFormatLookup,
} from "@/Utils/TimeUtils";
import { useTheme } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
import type { GroupedCheck } from "@/Types/Check";
import type { RootState } from "@/Types/state";
type XTickProps = {
x: number;
y: number;
payload: { value: any };
payload: { value: string };
range: string;
};
@@ -46,12 +49,9 @@ export const XTick = ({ x, y, payload, range }: XTickProps) => {
);
};
type ResponseTimeToolTipProps = {
active?: boolean | undefined;
payload?: any[];
label?: string | number;
type ResponseTimeToolTipProps = TooltipProps<ValueType, NameType> & {
range: string;
theme: any;
theme: Theme;
uiTimezone: string;
};
@@ -68,7 +68,8 @@ const ResponseTimeToolTip = ({
if (!active) return null;
const format = tooltipDateFormatLookup(range);
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
console.log(payload);
const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0);
return (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
@@ -52,7 +52,7 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
>
<RadialBar
dataKey="value"
background={{ fill: theme.palette[palette].light }}
background={{ fill: theme.palette.action.disabledBackground }}
>
<Cell visibility={"hidden"} />
<Cell fill={theme.palette[palette].main} />
@@ -75,13 +75,14 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
}}
>
<Typography
variant="h6"
variant="eyebrow"
color={theme.palette[palette].main}
textAlign={"center"}
>
{msg[palette]}
</Typography>
<Typography
variant="h6"
variant="h1"
textAlign={"center"}
>{`${avg?.toFixed()}ms`}</Typography>
</Stack>
+1
View File
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
export * from "./BulkActionsBar";
+6 -1
View File
@@ -68,7 +68,12 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
gap: theme.spacing(LAYOUT.MD),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(LAYOUT.XS),
"& svg": { stroke: theme.palette.text.secondary },
"& svg": {
height: 16,
width: 16,
opacity: 0.81,
stroke: theme.palette.text.secondary,
},
};
return (
+15 -12
View File
@@ -6,6 +6,7 @@ import { toggleSidebar } from "@/Features/UI/uiSlice.js";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import useSidebar from "@/Hooks/useSidebar";
import KingIcon from "@/assets/icons/checkmate-icon.svg?react";
export const Logo = (props: StackProps) => {
const { t } = useTranslation();
@@ -23,19 +24,21 @@ export const Logo = (props: StackProps) => {
sx={{ cursor: "pointer" }}
{...props}
>
<Typography
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.primary.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.primary.contrastText}
fontSize={18}
<Box
sx={{
width: theme.spacing(16),
height: theme.spacing(16),
display: "flex",
justifyContent: "center",
alignItems: "center",
"& svg": {
width: "100%",
height: "100%",
},
}}
>
C
</Typography>
<KingIcon />
</Box>
<Box
overflow={"hidden"}
sx={{
+8 -2
View File
@@ -9,7 +9,7 @@ import {
AlertTriangle,
Wifi,
Wrench,
Database,
ScrollText,
Settings,
HelpCircle,
MessageCircle,
@@ -17,6 +17,7 @@ import {
User,
Lock,
Users,
Tag,
} from "lucide-react";
export const getMenu = (t: Function) => {
@@ -41,6 +42,11 @@ export const getMenu = (t: Function) => {
path: "notifications",
icon: <Icon icon={Bell} />,
},
{
name: t("components.sidebar.menu.tags"),
path: "tags",
icon: <Icon icon={Tag} />,
},
{
name: t("components.sidebar.menu.checks"),
path: "checks",
@@ -64,7 +70,7 @@ export const getMenu = (t: Function) => {
{
name: t("components.sidebar.menu.logs"),
path: "logs",
icon: <Icon icon={Database} />,
icon: <Icon icon={ScrollText} />,
},
{
name: t("components.sidebar.menu.settings"),
+4 -2
View File
@@ -22,7 +22,9 @@ export const NavItem = ({
}) => {
const { collapsed } = useSidebar();
const theme = useTheme();
const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary;
const iconStroke = selected
? theme.palette.sidebar.accent
: theme.palette.text.secondary;
const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
const buttonBgHoverColor = selected
@@ -79,7 +81,7 @@ export const NavItem = ({
},
".MuiListItemButton-root:hover &": {
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: theme.palette.primary.main,
stroke: theme.palette.sidebar.accent,
},
},
}}
+2 -20
View File
@@ -6,7 +6,7 @@ import Divider from "@mui/material/Divider";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar.js";
import { Logo } from "@/Components/sidebar/Logo";
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { getMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { NavItem } from "@/Components/sidebar/NavItem";
import { StarPrompt } from "@/Components/sidebar/StarPrompt";
import { AuthFooter } from "@/Components/sidebar/Authfooter";
@@ -20,7 +20,7 @@ import { setCollapsed } from "@/Features/UI/uiSlice";
const URL_MAP: Record<string, string> = {
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
docs: "https://checkmate.so/docs",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
@@ -33,7 +33,6 @@ export const Sidebar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const menu = getMenu(t);
const bottomMenu = getBottomMenu(t);
const accountMenu = getAccountMenu(t);
useEffect(() => {
@@ -103,23 +102,6 @@ export const Sidebar = () => {
})}
</List>
<StarPrompt />
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(LAYOUT.SM) }}
>
{bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
selected={selected}
onClick={() => handleNavClick(item.path)}
/>
);
})}
</List>
<Divider sx={{ borderColor: theme.palette.divider }} />
<AuthFooter
+3 -1
View File
@@ -89,7 +89,9 @@ const uiSlice = createSlice({
},
setTimezone: (state, action: PayloadAction<{ timezone: string }>) => {
state.timezone = action.payload.timezone;
if (action.payload.timezone) {
state.timezone = action.payload.timezone;
}
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
+5 -1
View File
@@ -57,7 +57,11 @@ export const usePost = <B = any, R = any>() => {
...config?.headers,
},
});
toastSuccess(res.data?.msg || "Operation successful");
if (res.data?.success === false) {
toastError(res.data?.msg || "Operation failed");
} else {
toastSuccess(res.data?.msg || "Operation successful");
}
return res.data;
} catch (err: any) {
+6 -3
View File
@@ -4,8 +4,9 @@ export const useToast = () => {
const showToast = (message: string, options?: ToastOptions) => {
toast.dismiss();
const baseStyle: React.CSSProperties = {
whiteSpace: "pre-line",
wordBreak: "break-word",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
};
toast(message, {
...options,
@@ -19,6 +20,8 @@ export const useToast = () => {
showToast(msg, { ...opts, type: "error" });
const toastInfo = (msg: string, opts?: ToastOptions) =>
showToast(msg, { ...opts, type: "info" });
const toastWarning = (msg: string, opts?: ToastOptions) =>
showToast(msg, { ...opts, type: "warning" });
return { showToast, toastSuccess, toastError, toastInfo };
return { showToast, toastSuccess, toastError, toastInfo, toastWarning };
};
+105
View File
@@ -0,0 +1,105 @@
import { useState, useEffect } from "react";
import axios from "axios";
import { post } from "@/Utils/ApiClient";
import { useToast } from "@/Hooks/UseToast";
import { useTranslation } from "react-i18next";
import { logger } from "@/Utils/logger";
import type { Monitor } from "@/Types/Monitor";
interface ApiResponse {
success: boolean;
msg: string;
data: {
monitors: Monitor[];
failedCount: number;
};
}
interface UseBulkMonitorActionsReturn {
selectedRows: string[];
setSelectedRows: (rows: string[]) => void;
handleBulkPause: () => Promise<void>;
handleBulkResume: () => Promise<void>;
handleCancelSelection: () => void;
}
export const useBulkMonitorActions = (
refetch: () => void,
page?: number
): UseBulkMonitorActionsReturn => {
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const { toastSuccess, toastError, toastInfo, toastWarning } = useToast();
const { t } = useTranslation();
// Clear selection when page changes
useEffect(() => {
setSelectedRows([]);
}, [page]);
const executeBulkAction = async (pause: boolean) => {
try {
const res = await post<ApiResponse>("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause,
});
const affectedCount = res.data?.data?.monitors?.length ?? 0;
const failedCount = res.data?.data?.failedCount ?? 0;
if (affectedCount === 0) {
const key = pause
? "pages.common.monitors.bulkPause.alreadyPaused"
: "pages.common.monitors.bulkPause.alreadyRunning";
toastInfo(t(key, { count: selectedRows.length }));
} else if (failedCount > 0) {
toastWarning(
t("pages.common.monitors.bulkPause.partialFailure", {
successCount: affectedCount,
failedCount,
})
);
} else {
const key = pause
? "pages.common.monitors.bulkPause.paused"
: "pages.common.monitors.bulkPause.resumed";
toastSuccess(t(key, { count: affectedCount }));
}
setSelectedRows([]);
refetch();
} catch (err: unknown) {
let errMsg = "An error occurred";
if (axios.isAxiosError(err)) {
errMsg = err.response?.data?.msg || err.message || errMsg;
} else if (err instanceof Error) {
errMsg = err.message;
}
logger.error("Bulk pause/resume failed", err instanceof Error ? err : undefined, {
pause,
});
toastError(errMsg);
}
};
const handleBulkPause = async () => {
await executeBulkAction(true);
};
const handleBulkResume = async () => {
await executeBulkAction(false);
};
const handleCancelSelection = () => {
setSelectedRows([]);
};
return {
selectedRows,
setSelectedRows,
handleBulkPause,
handleBulkResume,
handleCancelSelection,
};
};
+37
View File
@@ -0,0 +1,37 @@
import { useState } from "react";
export const useClientPagination = <T>(rows: T[], initialRowsPerPage = 10) => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
setPage(0);
setRowsPerPage(Number(e.target.value));
};
const pagedRows = rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
return {
page,
rowsPerPage,
pagedRows,
paginationProps: {
component: "div" as const,
count: rows.length,
page,
rowsPerPage,
onPageChange: handlePageChange,
onRowsPerPageChange: handleRowsPerPageChange,
itemsOnPage: pagedRows.length,
},
};
};
+6 -2
View File
@@ -2,6 +2,7 @@ import { useMemo } from "react";
import dayjs from "dayjs";
import {
maintenanceWindowSchema,
maintenanceWindowEditSchema,
repeatOptions,
type MaintenanceWindowFormData,
} from "@/Validation/maintenanceWindow";
@@ -32,7 +33,7 @@ export const useMaintenanceWindowForm = ({
startTime: startDate.format("HH:mm"),
duration: data.duration ?? 0,
durationUnit: data.durationUnit ?? "minutes",
monitors: [data.monitorId],
monitors: data.monitorIds,
};
} else {
const now = dayjs();
@@ -47,6 +48,9 @@ export const useMaintenanceWindowForm = ({
};
}
return { schema: maintenanceWindowSchema, defaults };
return {
schema: data ? maintenanceWindowEditSchema : maintenanceWindowSchema,
defaults,
};
}, [data]);
};
+130 -100
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultHttpMethod, DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions {
@@ -12,118 +13,147 @@ const getBaseDefaults = (data?: Monitor | null) => ({
description: data?.description || "",
interval: data?.interval || 60000,
notifications: data?.notifications || [],
tags: data?.tags || [],
statusWindowSize: data?.statusWindowSize || 5,
statusWindowThreshold: data?.statusWindowThreshold || 60,
});
// Geo-check defaults — only http and ping support geo checks.
const getGeoCheckDefaults = (data?: Monitor | null) => ({
geoCheckEnabled: data?.geoCheckEnabled ?? false,
geoCheckLocations: data?.geoCheckLocations || [],
geoCheckInterval: data?.geoCheckInterval || 300000,
});
export const getMonitorDefaults = (
type: MonitorType,
data: Monitor | null = null
): MonitorFormData => {
const base = getBaseDefaults(data);
let defaults: MonitorFormData;
switch (type) {
case "http":
defaults = {
...base,
...getGeoCheckDefaults(data),
type: "http",
url: data?.url || "",
method: data?.method ?? DefaultHttpMethod,
ignoreTlsErrors: data?.ignoreTlsErrors || false,
useAdvancedMatching: data?.useAdvancedMatching || false,
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
customUpCodes: data?.customUpCodes || [],
};
break;
case "ping":
defaults = {
...base,
...getGeoCheckDefaults(data),
type: "ping",
url: data?.url || "",
};
break;
case "port":
defaults = {
...base,
type: "port",
url: data?.url || "",
port: data?.port || 80,
};
break;
case "docker":
defaults = {
...base,
type: "docker",
url: data?.url || "",
};
break;
case "game":
defaults = {
...base,
type: "game",
url: data?.url || "",
port: data?.port || 27015,
gameId: data?.gameId || "",
};
break;
case "grpc":
defaults = {
...base,
type: "grpc",
url: data?.url || "",
port: data?.port || 50051,
grpcServiceName: data?.grpcServiceName || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
case "pagespeed":
defaults = {
...base,
type: "pagespeed",
url: data?.url || "",
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
};
break;
case "hardware":
defaults = {
...base,
type: "hardware",
url: data?.url || "",
secret: data?.secret || "",
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
selectedDisks: data?.selectedDisks || [],
};
break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
case "dns":
defaults = {
...base,
type: "dns",
url: data?.url || "",
dnsServer: data?.dnsServer || "",
dnsRecordType: data?.dnsRecordType || "A",
};
break;
default:
defaults = {
...base,
...getGeoCheckDefaults(data),
type: "http",
url: "",
method: DefaultHttpMethod,
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: "",
expectedValue: "",
jsonPath: "",
customUpCodes: [],
};
}
return defaults;
};
export const useMonitorForm = ({
data = null,
defaultType = "http",
}: UseMonitorFormOptions = {}) => {
return useMemo(() => {
const type = data?.type || defaultType;
const base = getBaseDefaults(data);
let defaults: MonitorFormData;
switch (type) {
case "http":
defaults = {
...base,
type: "http",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
useAdvancedMatching: data?.useAdvancedMatching || false,
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
};
break;
case "ping":
defaults = {
...base,
type: "ping",
url: data?.url || "",
};
break;
case "port":
defaults = {
...base,
type: "port",
url: data?.url || "",
port: data?.port || 80,
};
break;
case "docker":
defaults = {
...base,
type: "docker",
url: data?.url || "",
};
break;
case "game":
defaults = {
...base,
type: "game",
url: data?.url || "",
port: data?.port || 27015,
gameId: data?.gameId || "",
};
break;
case "grpc":
defaults = {
...base,
type: "grpc",
url: data?.url || "",
port: data?.port || 50051,
grpcServiceName: data?.grpcServiceName || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
case "pagespeed":
defaults = {
...base,
type: "pagespeed",
url: data?.url || "",
};
break;
case "hardware":
defaults = {
...base,
type: "hardware",
url: data?.url || "",
secret: data?.secret || "",
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
selectedDisks: data?.selectedDisks || [],
};
break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default:
defaults = {
...base,
type: "http",
url: "",
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: "",
expectedValue: "",
jsonPath: "",
};
}
return { schema: monitorSchema, defaults };
return { schema: monitorSchema, defaults: getMonitorDefaults(type, data) };
}, [data, defaultType]);
};
+8
View File
@@ -78,6 +78,14 @@ function buildDefaults(data: Notification | null): NotificationFormData {
accessToken: data.accessToken || "",
};
}
if (data?.type === "ntfy") {
return {
type: "ntfy",
notificationName: data.notificationName || "",
address: data.address || "",
topic: data.topic || "",
};
}
// Default: email (covers both data === null and data.type === "email")
return {
type: "email",
+1
View File
@@ -18,6 +18,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
systemEmailHost: data?.systemEmailHost || "",
systemEmailUser: data?.systemEmailUser || "",
systemEmailAddress: data?.systemEmailAddress || "",
systemEmailDisplayName: data?.systemEmailDisplayName || "",
systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost",
systemEmailTLSServername: data?.systemEmailTLSServername || "",
systemEmailPort: data?.systemEmailPort,
+10 -2
View File
@@ -1,7 +1,12 @@
import { useMemo } from "react";
import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage";
import type { StatusPage } from "@/Types/StatusPage";
import {
DEFAULT_STATUS_PAGE_THEME,
DEFAULT_STATUS_PAGE_THEME_MODE,
} from "@/Types/StatusPage";
import type { Monitor } from "@/Types/Monitor";
import { resolveTimezone } from "@/Utils/TimeUtils";
interface UseStatusPageFormOptions {
data?: StatusPage | null;
@@ -26,9 +31,10 @@ export const useStatusPageForm = ({
const defaults: StatusPageFormData = {
companyName: data?.companyName || "",
url: data?.url || generateDefaultUrl(),
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
customDomain: data?.customDomain ?? null,
timezone: resolveTimezone(data?.timezone),
type: data?.type || ["uptime"],
color: data?.color || "#4169E1",
color: data?.color || "#13715B",
monitors: data?.monitors || [],
isPublished: data?.isPublished ?? false,
showCharts: data?.showCharts ?? true,
@@ -36,6 +42,8 @@ export const useStatusPageForm = ({
showAdminLoginLink: data?.showAdminLoginLink ?? false,
showInfrastructure: data?.showInfrastructure ?? false,
customCSS: data?.customCSS || "",
theme: data?.theme ?? DEFAULT_STATUS_PAGE_THEME,
themeMode: data?.themeMode ?? DEFAULT_STATUS_PAGE_THEME_MODE,
logo: transformLogo(data?.logo),
};
+18
View File
@@ -0,0 +1,18 @@
import { useMemo } from "react";
import { tagSchema, type TagFormData } from "@/Validation/tag";
import type { Tag } from "@/Types/Tag";
interface UseTagsFormOptions {
data?: Tag | null;
}
export const useTagsForm = ({ data }: UseTagsFormOptions) => {
return useMemo(() => {
const defaults: TagFormData = {
name: data?.name ?? "",
color: data?.color ?? "#13715B",
};
return { schema: tagSchema, defaults };
}, [data]);
};
+15 -1
View File
@@ -1,15 +1,18 @@
import { Stack } from "@mui/material";
import { useTheme } from "@mui/material";
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { HeaderTeamControls } from "./components/HeaderTeamControls";
import { TeamTable } from "./components/TeamTable";
import { InviteTeamMemberDialog } from "./components/InviteTeamMemberDialog";
import { AddTeamMemberDialog } from "./components/AddTeamMemberDialog";
import { EmptyState } from "@/Components/design-elements";
import { useGet } from "@/Hooks/UseApi";
import type { User, UserRole } from "@/Types/User";
export const TabTeam = () => {
const theme = useTheme();
const { t } = useTranslation();
const [filter, setFilter] = useState<UserRole | "">("");
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false);
@@ -33,6 +36,9 @@ export const TabTeam = () => {
refetch();
};
const totalUsers = users?.length ?? 0;
const noUsers = users !== undefined && totalUsers === 0;
return (
<Stack gap={theme.spacing(8)}>
<HeaderTeamControls
@@ -41,7 +47,15 @@ export const TabTeam = () => {
onInviteClick={handleOpenInviteDialog}
onAddMemberClick={handleOpenAddMemberDialog}
/>
<TeamTable users={filteredUsers} />
{noUsers ? (
<EmptyState
fullscreen
title={t("pages.account.team.fallback.title")}
description={t("pages.account.team.fallback.description")}
/>
) : (
<TeamTable users={filteredUsers} />
)}
<InviteTeamMemberDialog
open={inviteDialogOpen}
onClose={handleCloseInviteDialog}
@@ -9,6 +9,7 @@ import { useAddTeamMemberForm } from "@/Hooks/useAddTeamMemberForm";
import type { AddTeamMemberFormData } from "@/Validation/addTeamMember";
import type { UserRole, User } from "@/Types/User";
import { usePost } from "@/Hooks/UseApi";
import { LAYOUT } from "@/Utils/Theme/constants";
interface AddTeamMemberDialogProps {
open: boolean;
@@ -82,38 +83,40 @@ export const AddTeamMemberDialog = ({
maxWidth="sm"
fullWidth
>
<Stack
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Controller
name="firstName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
/>
)}
/>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
>
<Controller
name="firstName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
sx={{ flex: 1 }}
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
sx={{ flex: 1 }}
/>
)}
/>
</Stack>
<Controller
name="email"
control={control}
@@ -151,36 +154,41 @@ export const AddTeamMemberDialog = ({
</Select>
)}
/>
<Controller
name="password"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.common.form.option.password.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
/>
)}
/>
<Controller
name="confirm"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
/>
)}
/>
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
>
<Controller
name="password"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.common.form.option.password.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
sx={{ flex: 1 }}
/>
)}
/>
<Controller
name="confirm"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
sx={{ flex: 1 }}
/>
)}
/>
</Stack>
</Stack>
</Dialog>
);
@@ -9,6 +9,7 @@ import type { UserRole } from "@/Types/User";
import { useInviteForm } from "@/Hooks/useInviteForm";
import type { InviteFormData } from "@/Validation/invite";
import { usePost } from "@/Hooks/UseApi";
import { LAYOUT } from "@/Utils/Theme/constants";
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
@@ -95,10 +96,7 @@ export const InviteTeamMemberDialog = ({
</Button>
}
>
<Stack
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Controller
name="email"
control={control}
@@ -2,8 +2,10 @@ import Typography from "@mui/material/Typography";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Table } from "@/Components/design-elements";
import { Pagination } from "@/Components/design-elements/Table";
import type { Header } from "@/Components/design-elements/Table";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useClientPagination } from "@/Hooks/useClientPagination";
import type { User } from "@/Types/User";
interface TeamTableProps {
@@ -14,6 +16,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { pagedRows, paginationProps } = useClientPagination(users);
const headers: Header<User>[] = [
{
@@ -51,10 +54,13 @@ export const TeamTable = ({ users }: TeamTableProps) => {
};
return (
<Table
headers={headers}
data={users}
onRowClick={isAdmin ? handleRowClick : undefined}
/>
<>
<Table
headers={headers}
data={pagedRows}
onRowClick={isAdmin ? handleRowClick : undefined}
/>
{users.length > 0 && <Pagination {...paginationProps} />}
</>
);
};
+1 -1
View File
@@ -25,7 +25,7 @@ const Account = ({ open = "profile" }: AccountProps) => {
}, [open]);
return (
<BasePage>
<BasePage headerKey="account">
<Tabs
value={activeTab}
onChange={(_, newValue: number) => setActiveTab(newValue)}
+17 -1
View File
@@ -95,7 +95,23 @@ const RegisterPage = () => {
title={t("pages.auth.register.title")}
subtitle={t("pages.auth.register.subtitle")}
>
{!token && <Alert severity="info">{t("pages.auth.register.setupNotice")}</Alert>}
{!token && (
<Alert
severity="info"
icon={false}
sx={(theme) => ({
fontSize: 13,
lineHeight: 1.55,
color: theme.palette.text.secondary,
backgroundColor: theme.palette.action.hover,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
"& .MuiAlert-message": { padding: 0 },
})}
>
{t("pages.auth.register.setupNotice")}
</Alert>
)}
<Controller
name="firstName"
control={control}
@@ -1,8 +1,12 @@
import { Table, Pagination, ValueLabel, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StatusCodeLabel,
} from "@/Components/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/design-elements/Table";
import type { Monitor } from "@/Types/Monitor";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { Check } from "@/Types/Check";
@@ -29,7 +33,7 @@ export const ChecksTable = ({
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const getHeaders = (t: Function, uiTimezone: string) => {
const getHeaders = () => {
const headers: Header<Check>[] = [
{
id: "monitorName",
@@ -63,13 +67,10 @@ export const ChecksTable = ({
id: "statusCode",
content: t("pages.checks.table.headers.statusCode"),
render: (row) => {
const code = row.statusCode;
if (!code) return "N/A";
const value = code < 300 ? "positive" : code < 400 ? "neutral" : "negative";
return (
<ValueLabel
value={value}
text={String(code)}
<StatusCodeLabel
statusCode={row.statusCode}
message={row.message}
/>
);
},
@@ -85,7 +86,7 @@ export const ChecksTable = ({
return headers;
};
const headers = getHeaders(t, uiTimezone);
const headers = getHeaders();
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
+13 -1
View File
@@ -1,6 +1,7 @@
import Stack from "@mui/material/Stack";
import {
BasePage,
EmptyState,
TotalChecksBox,
UpChecksBox,
DownChecksBox,
@@ -102,8 +103,19 @@ const Checks = () => {
const downChecks = summaryResponse?.downChecks || 0;
const upChecks = totalChecks - (summaryResponse?.downChecks || 0);
const noMonitors = !isLoadingMonitors && (monitorsResponse?.length ?? 0) === 0;
if (noMonitors) {
return (
<EmptyState
fullscreen
title={t("pages.checks.fallback.title")}
description={t("pages.checks.fallback.description")}
/>
);
}
return (
<BasePage>
<BasePage headerKey="checks">
<Stack
direction={{ xs: "column", md: "row" }}
gap={4}
File diff suppressed because it is too large. Load diff
@@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack";
import Grid from "@mui/material/Grid";
import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography";
import { BaseBox, ValueLabel } from "@/Components/design-elements";
import { BaseBox, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useTranslation } from "react-i18next";
@@ -20,6 +20,24 @@ interface CardDetailsProps {
sx?: object;
}
const SectionHeading = ({ children }: { children: React.ReactNode }) => {
const theme = useTheme();
return (
<Typography
component="h2"
variant="eyebrow"
color={theme.palette.text.secondary}
>
{children}
</Typography>
);
};
const Cell = ({ children }: { children: React.ReactNode }) => (
<Typography variant="body1">{children}</Typography>
);
export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -28,27 +46,24 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
if (!incident) {
return null;
}
return (
<Stack
gap={theme.spacing(LAYOUT.MD)}
sx={sx}
>
<Typography textTransform={"uppercase"}>
{t("pages.incidents.dialog.details.title")}
</Typography>
<SectionHeading>{t("pages.incidents.dialog.details.title")}</SectionHeading>
<BaseBox padding={LAYOUT.MD}>
<Stack gap={theme.spacing(LAYOUT.MD)}>
<Typography textTransform={"uppercase"}>
{t("pages.incidents.dialog.details.overview")}
</Typography>
<Divider />
<SectionHeading>{t("pages.incidents.dialog.details.overview")}</SectionHeading>
<Grid
container
spacing={theme.spacing(LAYOUT.MD)}
alignItems="center"
>
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
<Grid size={2}>
<Cell>{t("pages.incidents.dialog.details.status")}</Cell>
</Grid>
<Grid size={10}>
<ValueLabel
value={incident.status ? "negative" : "positive"}
@@ -61,15 +76,17 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
</Grid>
{monitor && (
<>
<Grid size={2}>{t("pages.incidents.dialog.details.monitor")}</Grid>
<Grid size={2}>
<Cell>{t("pages.incidents.dialog.details.monitor")}</Cell>
</Grid>
<Grid size={10}>
<Typography>{monitor.name ?? "N/A"}</Typography>
<Cell>{monitor.name ?? "N/A"}</Cell>
</Grid>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.url")}</Typography>
<Cell>{t("pages.incidents.dialog.details.url")}</Cell>
</Grid>
<Grid size={10}>
<Typography>{monitor.url ?? "N/A"}</Typography>
<Cell>{monitor.url ?? "N/A"}</Cell>
</Grid>
</>
)}
@@ -78,51 +95,53 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
</BaseBox>
<BaseBox padding={LAYOUT.MD}>
<Stack gap={theme.spacing(LAYOUT.MD)}>
<Typography textTransform={"uppercase"}>
{t("pages.incidents.dialog.details.analysis")}
</Typography>
<Divider />
<SectionHeading>{t("pages.incidents.dialog.details.analysis")}</SectionHeading>
<Grid
container
spacing={theme.spacing(LAYOUT.MD)}
>
<Grid size={6}>
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
<Cell>{t("pages.incidents.dialog.details.timeline")}</Cell>
</Grid>
<Grid size={6}>
<Typography>{t("pages.incidents.dialog.details.detailsLabel")}</Typography>
<Cell>{t("pages.incidents.dialog.details.detailsLabel")}</Cell>
</Grid>
<Grid size={6}>
<Divider></Divider>
<Divider />
</Grid>
<Grid size={6}>
<Divider></Divider>
<Divider />
</Grid>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.startedAt")}</Typography>
<Cell>{t("pages.incidents.dialog.details.startedAt")}</Cell>
</Grid>
<Grid size={4}>
<Typography>
<Cell>
{formatDateWithTz(incident.startTime, "D MMM YYYY, h:mm A", uiTimezone)}
</Typography>
</Cell>
</Grid>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.statusCode")}</Typography>
<Cell>{t("pages.incidents.dialog.details.statusCode")}</Cell>
</Grid>
<Grid size={4}>
<Typography>{incident.statusCode ?? "N/A"}</Typography>
<Cell>
<StatusCodeLabel
statusCode={incident.statusCode}
message={incident.message}
/>
</Cell>
</Grid>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.downtime")}</Typography>
<Cell>{t("pages.incidents.dialog.details.downtime")}</Cell>
</Grid>
<Grid size={4}>
<Typography>{getIncidentsDuration(incident)}</Typography>
<Cell>{getIncidentsDuration(incident)}</Cell>
</Grid>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.message")}</Typography>
<Cell>{t("pages.incidents.dialog.details.message")}</Cell>
</Grid>
<Grid size={4}>
<Typography>{incident.message ?? "N/A"}</Typography>
<Cell>{incident.message ?? "N/A"}</Cell>
</Grid>
</Grid>
</Stack>
@@ -130,60 +149,53 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
{!incident.status && (
<BaseBox padding={LAYOUT.MD}>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Typography textTransform={"uppercase"}>
<SectionHeading>
{t("pages.incidents.dialog.details.resolutionDetails")}
</Typography>
<Divider />
</SectionHeading>
<Grid
container
spacing={theme.spacing(LAYOUT.MD)}
alignItems="center"
>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.resolvedAt")}</Typography>
<Cell>{t("pages.incidents.dialog.details.resolvedAt")}</Cell>
</Grid>
<Grid size={10}>
<Typography>
<Cell>
{incident.endTime
? formatDateWithTz(incident.endTime, "D MMM YYYY, h:mm A", uiTimezone)
: "N/A"}
</Typography>
</Cell>
</Grid>
<Grid size={2}>
<Typography>
{t("pages.incidents.dialog.details.resolutionType")}
</Typography>
<Cell>{t("pages.incidents.dialog.details.resolutionType")}</Cell>
</Grid>
<Grid size={10}>
<Typography>
<Cell>
{incident.resolutionType
? t(
`pages.incidents.dialog.details.resolutionTypes.${incident.resolutionType}`
)
: "N/A"}
</Typography>
</Cell>
</Grid>
{incident.resolvedBy && (
<>
<Grid size={2}>
<Typography>
{t("pages.incidents.dialog.details.resolvedBy")}
</Typography>
<Cell>{t("pages.incidents.dialog.details.resolvedBy")}</Cell>
</Grid>
<Grid size={10}>
<Typography>
{incident.resolvedByEmail ?? incident.resolvedBy}
</Typography>
<Cell>{incident.resolvedByEmail ?? incident.resolvedBy}</Cell>
</Grid>
</>
)}
{incident.comment && (
<>
<Grid size={2}>
<Typography>{t("pages.incidents.dialog.details.comment")}</Typography>
<Cell>{t("pages.incidents.dialog.details.comment")}</Cell>
</Grid>
<Grid size={10}>
<Typography>{incident.comment}</Typography>
<Cell>{incident.comment}</Cell>
</Grid>
</>
)}
@@ -1,8 +1,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid";
import { BaseBox, ValueLabel } from "@/Components/design-elements";
import { BaseBox, Icon, ValueLabel } from "@/Components/design-elements";
import { CircleCheck, TriangleAlert, Bell, Wrench, Globe } from "lucide-react";
import Box from "@mui/material/Box";
@@ -25,6 +24,7 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(2)}
minHeight={32}
>
<Stack
direction="row"
@@ -32,10 +32,10 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
gap={theme.spacing(2)}
>
{icon}
<Typography variant="body2">{label}</Typography>
<Typography variant="body1">{label}</Typography>
</Stack>
<Typography
variant="body2"
variant="body1"
fontWeight={600}
>
{value}
@@ -67,15 +67,11 @@ export const SummaryCard = ({
>
<Typography
component="h2"
sx={{
textTransform: "uppercase",
fontWeight: 500,
fontSize: 13,
}}
variant="eyebrow"
color={theme.palette.text.secondary}
>
{title}
</Typography>
<Divider />
{children}
</Stack>
</BaseBox>
@@ -97,10 +93,13 @@ export const SummaryCardActiveIncidents = ({
const activeCount = summary.totalActive;
const hasActive = activeCount > 0;
const color = hasActive ? theme.palette.error.main : theme.palette.success.main;
const icon = hasActive ? (
<TriangleAlert color={color} />
) : (
<CircleCheck color={color} />
const icon = (
<Box sx={{ color, display: "inline-flex" }}>
<Icon
icon={hasActive ? TriangleAlert : CircleCheck}
size={32}
/>
</Box>
);
const msg = t("pages.incidents.summaryCard.activeIncidents.active", {
count: activeCount,
@@ -133,10 +132,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
container
alignItems="center"
spacing={2}
sx={{
width: "100%",
py: theme.spacing(0.5),
}}
sx={{ width: "100%", minHeight: 32 }}
>
<Grid
size={{ xs: 12, lg: 5 }}
@@ -147,7 +143,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
gap: theme.spacing(2),
}}
>
<Globe />
<Icon icon={Globe} />
<Typography
variant="body1"
fontWeight={500}
@@ -159,9 +155,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
<Grid
size={{ xs: 12, md: 6, lg: 3 }}
sx={{
display: "flex",
}}
sx={{ display: "flex" }}
>
<ValueLabel
value={incident.status ? "negative" : "positive"}
@@ -173,7 +167,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
size={{ xs: 12, md: 6, lg: 4 }}
sx={{
textAlign: { xs: "left", md: "right" },
fontWeight: 500,
fontWeight: 600,
}}
>
<Typography variant="body1">{duration}</Typography>
@@ -190,22 +184,17 @@ export const SummaryCardLatestIncidents = ({
summary,
}: SummaryCardLatestIncidentsProps) => {
const { t } = useTranslation();
const theme = useTheme();
const latestIncidents = summary?.latestIncidents ?? [];
return (
<SummaryCard title={t("pages.incidents.summaryCard.latestIncidents.title")}>
<Stack gap={theme.spacing(4)}>
{latestIncidents.slice(0, 3).map((incident, index) => (
<Box key={incident.id}>
<SummaryIncidentItem incident={incident} />
{index < latestIncidents.length - 1 && (
<Divider sx={{ mt: theme.spacing(2) }} />
)}
</Box>
))}
</Stack>
{latestIncidents.slice(0, 3).map((incident) => (
<SummaryIncidentItem
key={incident.id}
incident={incident}
/>
))}
</SummaryCard>
);
};
@@ -224,17 +213,17 @@ export const SummaryCardStats = ({ summary }: SummaryCardStatsProps) => {
return (
<SummaryCard title={t("pages.incidents.summaryCard.incidentStats.title")}>
<SummaryItem
icon={<Bell size={18} />}
icon={<Icon icon={Bell} />}
label={t("pages.incidents.summaryCard.incidentStats.totalIncidents")}
value={summary?.total || 0}
/>
<SummaryItem
icon={<TriangleAlert size={18} />}
icon={<Icon icon={TriangleAlert} />}
label={t("pages.incidents.summaryCard.incidentStats.mostAffectedMonitor")}
value={mostAffected}
/>
<SummaryItem
icon={<Wrench size={18} />}
icon={<Icon icon={Wrench} />}
label={t("pages.incidents.summaryCard.incidentStats.avgResolutionTime")}
value={
summary.total > 0
@@ -75,7 +75,7 @@ export const ControlsIncidentFilter = ({
variant="contained"
onClick={onClearFilters}
>
{t("pages.incidents.filters.clearFilters")}
{t("common.buttons.clearFilters")}
</Button>
)}
</Stack>
@@ -1,9 +1,7 @@
import { useState, useEffect } from "react";
import Box from "@mui/material/Box";
import { Dialog, TextField } from "@/Components/inputs";
import { usePut } from "@/Hooks/UseApi";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
interface DialogResolutionProps {
open: boolean;
@@ -19,7 +17,6 @@ export const DialogResolution = ({
onResolved,
}: DialogResolutionProps) => {
const { t } = useTranslation();
const theme = useTheme();
const [comment, setComment] = useState("");
const { put: resolveIncident, loading: isResolving } = usePut();
@@ -50,22 +47,19 @@ export const DialogResolution = ({
onCancel={handleCancel}
onConfirm={handleConfirm}
confirmColor="error"
cancelColor="primary"
loading={isResolving}
maxWidth="sm"
fullWidth
>
<Box sx={{ mt: theme.spacing(4) }}>
<TextField
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
placeholder={t(
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
)}
value={comment}
onChange={(e) => setComment(e.target.value)}
fullWidth
/>
</Box>
<TextField
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
placeholder={t(
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
)}
value={comment}
onChange={(e) => setComment(e.target.value)}
fullWidth
/>
</Dialog>
);
};
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff. Show more