Compare commits

...
Author SHA1 Message Date
gorkem-bwl e760db1192 fix(monitors): correct proxy transport and error attribution
Second review round found two blocking defects, both verified against a stub
proxy rather than inferred.

HttpsProxyAgent was used for every target, and it always issues CONNECT:

    PROXY RECEIVED: "CONNECT example.com:80 HTTP/1.1"

Squid's stock ACL permits CONNECT only to 443, so every http:// monitor would
have broken the moment HTTP_PROXY was set — in exactly the hardened networks
this feature targets. Plain-http targets now use http-proxy-agent, which
sends an absolute-URI request. The agent cache keys on target scheme.

A refused CONNECT is replayed onto the socket, so the proxy's own status was
parsed as the target's: a proxy ACL denial reported "the site returned 403",
and a monitor whose customUpCodes included 403 or 407 was marked UP while the
target was never reached. Proxy-tunnel rejections are now detected via the
agent's proxyConnect event and reported as a network error naming the proxy.
The listener is per request and removed in a finally block, so a failure
cannot be attributed to a concurrent check.

Also from review:

- The fallback for an invalid PROXY_MONITOR_TYPES no longer fires when every
  entry was a deliberate but unproxyable type ("ping,port"). Falling back
  there would have proxied http and pagespeed, which the operator never
  nominated. Typos still fall back; unproxyable lists stay empty and warn.
- Dropped the now-redundant strict/insecure agent split: TLS is applied per
  request, and Node already separates socket pools by TLS options, so the
  split only doubled tunnels to the proxy.
- Renamed to ProxyPolicy.ts to match the PascalCase peers in the directory.
- The warning no longer echoes the raw config value.

Adds 9 tests covering scheme selection, tunnel rejection, and the narrowed
fallback. Verified by reverting: the tunnel-failure test fails without the fix.
2026-08-20 10:58:55 -04:00
gorkem-bwl 3b03850a3b fix(monitors): keep ignoreTlsErrors effective for proxied checks
Review found that the proxy path silently dropped ignoreTlsErrors.
rejectUnauthorized was passed to the HttpsProxyAgent constructor, where it
governs the socket to the proxy rather than the tunnelled handshake with the
target. Verified directly: the value lands in connectOpts, while the target
handshake reads got's per-request https options, which were unset and so
defaulted to strict.

The consequence was worse than a no-op. A monitor with ignoreTlsErrors
against a self-signed endpoint works today; configuring a proxy would have
made it fail verification, flip to down and fire a false incident with
notifications. Stating the option per request fixes it, and the agent cache
keeps its TLS split so a relaxed agent is never shared with a strict monitor.

Also from review:

- A fully-invalid PROXY_MONITOR_TYPES disabled proxying entirely, which in a
  restricted-egress deployment would take every HTTP monitor down at once —
  the exact failure the parser was meant to prevent. It now falls back to the
  defaults and warns loudly. "none" remains the way to disable deliberately.
- The "none" sentinel only matched when it was the sole entry, so "none,http"
  half-enabled proxying. It is now honoured wherever it appears.
- "unknown" is a real MonitorType but never a deliberate choice, so it is
  reported as a typo rather than as an unproxyable type.
- Dropped the unused warning field from ProxyResolution.
- Documented that lowercase proxy variables are honoured and take precedence,
  which matters on base images that already set them.

Adds 10 tests, including the request-level TLS assertions the previous suite
lacked. Verified by reverting the fix: both TLS tests fail without it.
2026-08-20 10:46:25 -04:00
gorkem-bwl 705c19f7c8 feat(monitors): route checks through an outbound HTTP proxy
Deployments with restricted egress could not run checks at all: got does not
read HTTP_PROXY/HTTPS_PROXY (unlike curl or axios), and NODE_USE_ENV_PROXY
only affects Node's built-in fetch, which got does not use. Even a correctly
built proxy agent was discarded, because HttpProvider overwrote
options.agent on every request.

Proxy selection is resolved per check and applied where the agent is chosen.
NO_PROXY handling is delegated to proxy-from-env rather than reimplemented,
so wildcards, host lists and port-qualified entries behave exactly as
operators expect from curl.

Infrastructure monitors are excluded by default. Hardware and pagespeed
checks both delegate to HttpProvider, so a naive global proxy would also
capture Capture agents — which normally sit on the internal network, where
proxying them fails or hairpins. PROXY_MONITOR_TYPES is the escape hatch for
deployments whose agents genuinely are behind the proxy, and accepts "none"
to disable proxying without unsetting the environment variables. Values that
are not real monitor types, or that open raw sockets and cannot be proxied
(ping, port, docker, grpc, websocket, dns, game), are dropped and logged so
a typo cannot silently disable proxying.

Agents are cached by proxy URL and TLS mode. Keying on TLS matters: sharing
one agent between a strict monitor and an ignoreTlsErrors monitor would leak
the relaxed setting across monitors. Caching also preserves pooling, so 50
monitors share tunnels instead of dialling the proxy per check.

Both new dependencies were already present transitively (76K total).

Notification providers still call out directly and are unaffected; that is a
known follow-up for the same class of deployment.
2026-08-20 10:39:36 -04:00
Alexander Holliday a719edb0fc Merge pull request #3856 from bluewave-labs/feat/theme-button
feat: add theme mode to auth footer menu
2026-08-19 12:05:25 -07:00
Alexander Holliday 53702ec04b Merge pull request #3855 from bluewave-labs/fix/keyboard-navigation
feat: improve keyboard navigation on create monitor page
2026-08-19 12:04:12 -07:00
Alex Holliday 84a73d9616 add theme mode to auth footer menu 2026-08-19 12:02:45 -07:00
Alex Holliday 7b2358bd48 improve keyboard navigation on create monitor page 2026-08-19 11:28:46 -07:00
Alexander Holliday 79957693fe Merge pull request #3854 from bluewave-labs/fix/infra-instant-gauge
fix: add recent checks for infra monitors
2026-08-19 10:29:44 -07:00
Alex Holliday 7fd368def3 update tests 2026-08-19 10:26:17 -07:00
Alex Holliday dd44e6c34f add recent checks for infra monitors 2026-08-19 10:20:21 -07:00
Alexander Holliday 092f890f7f Merge pull request #3851 from bluewave-labs/chore/dependency-bump
chore: bump server dependencies
2026-08-18 11:25:03 -07:00
Alex Holliday 2468d3e287 bump dependencies 2026-08-18 11:21:20 -07:00
Alexander Holliday bc25c15c34 Merge pull request #3844 from bluewave-labs/fix/redact-sensitive-fields-client-logs
fix(security): redact sensitive fields from client-side logs
2026-08-18 10:49:47 -07:00
Alex Holliday 1b6218ea56 remove tests and harness 2026-08-18 10:46:57 -07:00
Alexander Holliday 6eba53d716 Merge pull request #3837 from egeoztass/fix/email-error-propagation
fix: propagate SMTP transport errors from EmailService.sendEmail
2026-08-18 10:36:02 -07:00
Alexander Holliday 8bfb3a7de5 Merge pull request #3838 from egeoztass/fix/matrix-notifications
fix: resolve Matrix room aliases and encode room IDs in the send path
2026-08-18 10:32:45 -07:00
Alexander Holliday 5cca0fb2d3 Merge pull request #3839 from egeoztass/feat/ntfy-authentication
feat: add authentication support to ntfy notifications
2026-08-18 10:30:08 -07:00
Alexander Holliday 617ac3a9bb Merge pull request #3847 from bluewave-labs/fix/dns-monitors-status-page-picker
fix(status-page): allow DNS monitors to be added to status pages
2026-08-18 10:21:17 -07:00
Alexander Holliday e92fe39e3b Merge pull request #3849 from bluewave-labs/feat/status-page-ranges-client
feat: status page ranges client
2026-08-18 10:03:12 -07:00
Alex Holliday 248dad01d4 add retention hint 2026-08-18 10:01:05 -07:00
Alex Holliday 840c936501 remove unused vars 2026-08-18 09:38:34 -07:00
gorkem-bwl 7096afcdf2 fix(status-page): allow DNS monitors to be added to status pages
The monitor picker on the status page create/configure form fetched
candidates using a hardcoded list of monitor types that omitted "dns", so
DNS monitors were filtered out server-side and never appeared in the
dialog. Everything else was already in place: the DNS provider stores
checks like any other monitor type, and the status page renders them once
attached.

Rather than appending "dns" to the hardcoded list, derive the query from
the existing SelectableMonitorTypes tuple in @/Types/Monitor. The two lists
had silently drifted apart, and deriving prevents the next new monitor type
from hitting the same bug. "hardware" is appended explicitly because
infrastructure monitors are created on their own page and so are absent
from the selectable set.

Net effect on the requested types: gains "dns", loses none.

Fixes #3721
2026-08-18 02:37:54 -04:00
Alex Holliday c15d6e0851 wire client 2026-08-17 14:56:53 -07:00
Alex Holliday 506133af29 add daily tooltip 2026-08-17 13:55:01 -07:00
Alex Holliday 03f168fa84 add daily bucket type 2026-08-17 13:54:38 -07:00
Alex Holliday 4b02bbec37 add helper methods 2026-08-17 13:54:14 -07:00
Alex Holliday 42aae20fa3 add param to URL 2026-08-17 13:30:03 -07:00
Alex Holliday c71c833b77 add range to query 2026-08-17 13:20:36 -07:00
Alex Holliday cee7db64ee add range types 2026-08-17 13:16:55 -07:00
Alexander Holliday 40e06a2f2b Merge pull request #3845 from bluewave-labs/feat/status-page-ranges-server
feat: status page ranges server
2026-08-17 13:14:40 -07:00
Alex Holliday 7478bd7d17 remove duplicate validation 2026-08-17 13:08:43 -07:00
Alex Holliday 7b1d6c763f tighten types, remove duplicate calls 2026-08-17 13:05:03 -07:00
Alex Holliday ff2e1fd648 use reduce 2026-08-17 12:59:23 -07:00
Alex Holliday 9001a8d294 add tests 2026-08-17 12:58:56 -07:00
Alex Holliday ab7f2cd8f4 update openapi docs 2026-08-17 12:36:39 -07:00
Alex Holliday c1f0bf3dd4 wire controller 2026-08-17 12:31:01 -07:00
Alex Holliday 3fc7020a90 move methods to checks, wire up service 2026-08-17 12:26:49 -07:00
Alex Holliday 6f9642c298 tighten timezone validation 2026-08-17 11:48:56 -07:00
Alex Holliday 3a22d3e819 add range buckets for status pages 2026-08-17 11:48:44 -07:00
Alexander Holliday 697574a2a9 Merge pull request #3834 from colinjtsen2010/feat/domain-monitoring
feat: add domain registration expiry monitoring
2026-08-17 10:51:11 -07:00
Alex Holliday 0cbd9ab8a2 point lockfile at official npm registry 2026-08-17 10:49:39 -07:00
Alexander Holliday 9032fcd7f0 Merge pull request #3831 from mdiniz97/feat/rocket-chat-rich-notifications
feat: enrich Rocket.Chat notifications
2026-08-17 10:22:38 -07:00
gorkem-bwl ed268b3099 test(client): cover credential redaction in the logger
Adds a regression suite for #3843 so the leak cannot return silently. The
client had no test runner, so this introduces Vitest, which reuses the
existing Vite resolution and the `@/` alias rather than needing a separate
transform pipeline. jsdom is required because the logger reads
window.location and navigator.userAgent.

The tests drive the exported `logger` rather than the private redaction
helpers, so they cover the wiring as well as the logic. That distinction
matters here: the first attempt at this fix redacted the JSON branch but
left the debug branch printing raw objects, a gap that testing the helper
in isolation would not have caught. Each case runs at both error and debug
level for the same reason.

Covered: the reported login payload, credentials carried on an AxiosError
(config.data and the Authorization header), non-JSON request bodies,
passwords on class instances, key-matching variants, and circular payloads.
One asserts non-sensitive context survives, so redaction cannot regress
into blanket suppression.

Verified by reverting the fix: 16 of 23 tests fail against the original
behaviour, including a pre-existing crash on circular payloads.
2026-08-17 12:35:59 -04:00
gorkem-bwl e26825ef99 fix(security): close redaction gaps found in review
Follow-up to the initial redaction pass, addressing four defects:

Class instances bypassed redaction entirely. The prototype guard was meant
to skip FormData/Blob but caught every non-literal object, returning it by
reference — so a credential on a DTO or form-state instance was logged
verbatim, and the deep-copy contract was violated. The check is now an
explicit deny-list of opaque types; everything else is walked.

The raw Error was still logged by the debug branch. An AxiosError carries
config.data (the serialized request body) and config.headers.Authorization
(set for every request in ApiClient), so the password redacted from the
context was printed straight back out via the error object. Errors now go
through toSafeError(). Because Axios serializes the body to a string, which
key-based redaction cannot see into, it is parsed before redaction; a body
that is not JSON is replaced wholesale rather than logged raw.

The Blob guard was gated on `typeof FormData !== "undefined"`, so in an
environment with FormData but no Blob the instanceof check threw a
ReferenceError — from inside a catch block, turning a handled API error
into an unhandled crash. Each constructor is now resolved defensively.

Also widened the key list (cookie, jwt, privateKey, sessionId, csrf, pwd)
and dropped a redundant type annotation.

Verified with 18 assertions covering the reported payload, class instances,
cycles, shared siblings, axios error shapes, and a missing Blob global.
2026-08-17 12:31:40 -04:00
gorkem-bwl 741ee7a92a fix(security): redact sensitive fields from client-side logs
Failed requests logged the full request body to the browser console, so a
failing POST to /auth/login wrote the user's plaintext password where it
could be captured by error-reporting tools, screen shares, or support
sessions.

Redaction is applied centrally in the logger rather than at the call sites,
so every current and future caller is covered:

- createLogData() redacts context before it is serialized, covering the
  JSON branches of error() and warn().
- The debug-level branches of error() and warn() printed the raw context
  object directly, bypassing createLogData(); they now print the redacted
  copy.
- debug() and info() redact their free-form data argument.

Keys are matched case-insensitively on a substring after stripping dashes
and underscores, so password, newPassword, X-Api-Key and Authorization are
all caught. The walk copies rather than mutates the caller's object, cuts
genuine cycles without mislabelling repeated siblings, and renders
FormData/Blob as a type marker instead of walking them.

Affected paths were POST (/auth/login, /auth/register) and PATCH/PUT, which
carry monitor secrets. Error-level logging is on by default, so this was
not limited to debug builds.

Fixes #3843
2026-08-17 12:27:02 -04:00
egeoztass 099eaa38c3 fix: resolve Matrix room aliases and encode room IDs in the send path
Matrix notifications failed for anyone who entered a room alias. The send
endpoint only accepts a room ID, and an unresolved alias broke the request
outright: the leading "#" opens a URI fragment, so the PUT landed on
/_matrix/client/v3/rooms/ instead of the send endpoint. Element shows users
the alias, so this is the value most people paste in.

- Resolve "#alias:server" through GET /_matrix/client/v3/directory/room/{alias}
  before sending, per the client-server spec
- Percent-encode the room ID in the send path; ":" was previously sent raw
- Strip trailing slashes from the homeserver URL to avoid "//_matrix/..."

Alias resolution failures return false and log, matching how the provider
already handles send failures.

Closes #3478
2026-08-14 17:55:36 +03:00
egeoztass c35b22e1ac fix: propagate SMTP transport errors from EmailService.sendEmail
sendEmail caught transport failures internally and resolved to false or
undefined, so every caller's .catch() was dead code and send failures were
silently suppressed. Password recovery in particular reported "email sent
successfully" while the user received nothing.

sendEmail now throws an AppError carrying the underlying cause, and returns
Promise<string> instead of Promise<string | false | undefined>.

Call sites updated for the new contract:
- UserService.registerUser: the existing .catch() now actually fires
- UserService.requestRecovery: the failure reaches the client instead of a
  misleading success response
- InviteService and the test-email endpoint: keep their user-facing messages
  and attach the SMTP cause
- EmailProvider: catches and returns false, because NotificationsService fans
  providers out through Promise.all and a rejection there would abort the
  other channels' alerts

Closes #3757
2026-08-14 14:11:16 +03:00
egeoztass 1c9739d1da feat: add authentication support to ntfy notifications
ntfy servers that require auth rejected every Checkmate alert because the
provider posted without an Authorization header, so alerts to protected
topics failed silently.

Adds ntfyAuthType ("none" | "token" | "basic"):
- token: Authorization: Bearer <accessToken>
- basic: Authorization: Basic base64(ntfyUsername:accessToken)

Both types store their secret in the existing accessToken field rather than
introducing a new secret, so credential handling stays in one place.

Incomplete credentials fail the send instead of falling back to an anonymous
post, and validation rejects partial auth config up front so a channel can't
be saved looking authenticated while posting anonymously.

Backend only; frontend follows once this lands.

Closes #3717
2026-08-14 14:01:06 +03:00
Marcos Paulo Diniz d4eaad6170 fix(notifications): route Rocket.Chat links by monitor type
Build title_link from the monitor type (hardware -> infrastructure,
pagespeed -> pagespeed, uptime types -> uptime) and omit it when
clientHost is not configured, instead of always linking to the
infrastructure page.
2026-08-13 11:56:28 -03:00
colinjtsen2010 75516a220d fix: handle IPs, avoid wrong parent-domain lookups, cache whois results
- Bare IP addresses and non-domain hosts (localhost, IPv6) now return a
  null expiry instead of throwing, so they no longer spam error logs.
- Extract the registrable domain with tldts instead of walking up
  hostname labels, so a subdomain can never accidentally return its
  parent domain's expiry date.
- Cache WHOIS lookups per registrable domain (1h positive, 10min
  negative) so visiting details pages does not re-query the registry
  every time.

Addresses review feedback on #3834.
2026-08-13 12:02:49 +08:00
Alexander Holliday f97ca8565a Merge pull request #3829 from bluewave-labs/chore/form-refactor
chore: form refactor
2026-08-12 10:24:24 -07:00
Alex Holliday b5f8faf1e8 add missing rocket chat label 2026-08-12 10:22:56 -07:00
Alexander Holliday b088101160 Merge branch 'develop' into chore/form-refactor 2026-08-12 10:15:10 -07:00
Alex Holliday 521827e1d0 memoize options 2026-08-11 17:50:47 -07:00
Alex Holliday 7dd378f085 remove duplicates 2026-08-11 17:26:15 -07:00
Alex Holliday a7fb235c56 fix monitor types 2026-08-11 17:24:03 -07:00
Alex Holliday 7f68d23566 fix non sortable styling 2026-08-11 17:21:51 -07:00
Alex Holliday caebb93748 derive options from roles, wire error state 2026-08-11 17:15:15 -07:00
Alex Holliday 5640068907 fix placeholder 2026-08-11 17:10:48 -07:00
Alex Holliday 441ecddf92 fix unresolved IDs 2026-08-11 17:08:18 -07:00
colinjtsen2010 e5959ca91d feat: add domain registration expiry monitoring
Adds a GET /monitors/domain/:monitorId endpoint that looks up the
registrable domain's WHOIS expiry date (walking up from subdomains),
and displays it next to the certificate expiry on the uptime details
page.

Fixes #2094
2026-08-11 00:34:21 +08:00
Alex Holliday 2765ebfc3b bug fixes 2026-08-07 11:50:47 -07:00
Marcos Paulo Diniz 8d1fd61bb4 fix(notifications): omit empty incident links 2026-08-07 14:55:05 -03:00
Marcos Paulo Diniz 865fa7cce6 feat(notifications): add rich alert fields 2026-08-07 14:55:05 -03:00
Marcos Paulo Diniz 2c780b8101 feat(notifications): add Rocket.Chat attachments 2026-08-07 14:55:05 -03:00
Marcos Paulo Diniz ee25c6b479 feat(notifications): enrich Rocket.Chat tests 2026-08-07 14:55:05 -03:00
Alex Holliday d0c8da1653 remove dead default values 2026-08-07 10:41:16 -07:00
Alex Holliday 5916d0ca33 refactor recovery and set new password 2026-08-07 10:32:01 -07:00
Alex Holliday 69db716f0b refactor login page 2026-08-07 10:29:46 -07:00
Alex Holliday 350083d179 refactor Profile tab 2026-08-07 10:25:04 -07:00
Alex Holliday 11a6ee71e9 refacotr add team/invite team member 2026-08-07 10:22:43 -07:00
Alex Holliday eaf93668f5 refactor password tab 2026-08-07 10:13:56 -07:00
Alex Holliday 59267ef451 refactor edit user 2026-08-07 10:08:27 -07:00
Alex Holliday 35f9381573 refactor register page 2026-08-07 10:06:09 -07:00
Alex Holliday 3df85b7522 refacotr add team member dialog 2026-08-07 09:54:08 -07:00
Alex Holliday 012e09d30c refactor tags create page 2026-08-07 09:47:11 -07:00
Alexander Holliday 4b921f4986 Merge pull request #3826 from Glazzze/docs/clarify-mongodb-deployment-3823
docs(readme): clarify MongoDB requirement for single-image deployment
2026-08-07 09:27:11 -07:00
Alex Holliday 0eff052258 refactor maintenance create page 2026-08-06 15:05:16 -07:00
Alex Holliday 52c80b9a39 refactor status page create page 2026-08-06 14:40:21 -07:00
Alex Holliday 6ceb28d3a1 remove dead keys 2026-08-06 12:33:35 -07:00
Alex Holliday 08bd2d56f3 refactor settings page 2026-08-06 12:31:20 -07:00
Alex Holliday 05f265743b remove unsued 2026-08-06 11:55:19 -07:00
Alex Holliday bc2115f578 refacotr notification create page 2026-08-06 11:54:46 -07:00
Alex Holliday c1e9fd6079 refactor sliders 2026-08-06 11:00:37 -07:00
Alex Holliday 226864079c refactor sliders 2026-08-06 10:50:37 -07:00
Alex Holliday 7cab326ad3 refactor selects 2026-08-06 10:17:32 -07:00
Glazzze 94f1f7e576 docs(readme): clarify MongoDB requirement for single-image deployment 2026-08-06 11:41:01 +08:00
Alex Holliday 20d1d1407b remove dead key 2026-08-05 11:53:49 -07:00
Alex Holliday da7e8a1ae8 add missing remove button label 2026-08-05 11:52:47 -07:00
Alex Holliday c578387261 fix label and description regression 2026-08-05 11:51:20 -07:00
Alex Holliday c5ef61e64c refactor radio and multi select autocomplete 2026-08-05 11:44:47 -07:00
Alex Holliday 6e1e5dd1a5 create bound components for text input and radio input 2026-08-05 10:51:48 -07:00
Alexander Holliday 514ee0d30b Merge pull request #3822 from mdiniz97/feat/rocket-chat-notifications
feat: Rocket.Chat notifications
2026-08-05 09:33:42 -07:00
Alexander Holliday 623db938f5 Merge pull request #3825 from bluewave-labs/fix/check-snapshot
fix:  check snapshot
2026-08-05 09:20:57 -07:00
Marcos Paulo Diniz 1c291bb893 refactor(client): use raw channel types
Refs #3821
2026-08-05 08:35:56 -03:00
Marcos Paulo Diniz 466d356ad1 refactor(validation): use Zod URL protocols
Refs #3821
2026-08-05 08:34:12 -03:00
Marcos Paulo Diniz 0c184e6d0c fix(notifications): guard incident link
Refs #3821
2026-08-05 08:33:29 -03:00
Alex Holliday 303f47a117 add opt out flag for recent checks 2026-08-04 15:30:34 -07:00
Alex Holliday 5fa74bd883 replace findAll with findAllForScheduling 2026-08-04 15:16:03 -07:00
Alex Holliday 4c8342f379 fix pretty name bug, ordering bug 2026-08-04 14:46:52 -07:00
Alex Holliday dfcb3b4b21 slim check snapshot model/types 2026-08-04 11:27:32 -07:00
Marcos Paulo Diniz 2e19b85a2d docs(notifications): document Rocket.Chat
Refs #3821
2026-08-02 01:59:58 -03:00
Marcos Paulo Diniz 5774cb0e06 feat(client): expose Rocket.Chat channel
Refs #3821
2026-08-02 01:59:34 -03:00
Marcos Paulo Diniz 750fc9e238 feat(notifications): add Rocket.Chat provider
Refs #3821
2026-08-02 01:59:11 -03:00
Alexander Holliday 3646af9c04 Merge pull request #3820 from bluewave-labs/chore/ms-cleanup
chore: ms cleanup
2026-07-31 13:06:35 -07:00
Alex Holliday d4782a97a3 add missing verbose flag 2026-07-31 13:05:10 -07:00
Alex Holliday e0122d3761 hoist props 2026-07-31 13:04:10 -07:00
Alex Holliday e3b0b33b15 replace duration in monitorstatus, remove dead import 2026-07-31 13:00:58 -07:00
Alex Holliday 19b5976f85 extract formatting durations 2026-07-31 12:58:58 -07:00
Alex Holliday 336dbe4de2 use formatMs function to consistently format point times 2026-07-31 12:40:18 -07:00
Alex Holliday 65c6f567b5 add ms formatting util 2026-07-31 11:55:05 -07:00
Alexander Holliday 6e6ac5dbf6 Merge pull request #3819 from bluewave-labs/feat/client-side-presentation
feat: client side presentation
2026-07-31 11:52:54 -07:00
Alex Holliday e81fdb9810 remove unused hook 2026-07-31 11:50:02 -07:00
Alex Holliday 9712579c4d remove unused imports 2026-07-31 11:45:41 -07:00
Alex Holliday 98e0ffc983 update tooltips 2026-07-31 11:41:18 -07:00
Alex Holliday ac251c4d3f update tests 2026-07-31 11:36:20 -07:00
Alex Holliday 08f167c18e cap values at p95 on client 2026-07-31 11:36:10 -07:00
Alex Holliday 8a608961e6 remove server side normalization 2026-07-31 10:31:39 -07:00
Alexander Holliday 955c400731 Merge pull request #3808 from anjupathak03/fix/tag-filtering
test: cover monitor tag filtering
2026-07-31 10:06:59 -07:00
Alexander Holliday 093553f9ba Merge pull request #3818 from bluewave-labs/feat/chart-cleanup
feat: chart cleanup
2026-07-30 12:16:24 -07:00
Alex Holliday e5282b9099 opacity -> 0.2 2026-07-30 12:04:25 -07:00
Alex Holliday 6b4444038c add tests 2026-07-30 11:54:19 -07:00
Alex Holliday 89464e9be4 add legend 2026-07-30 11:54:12 -07:00
Alex Holliday 6856914d41 extract hardcoded strings 2026-07-30 10:56:24 -07:00
Alex Holliday c102cd8d26 openapi schemas 2026-07-30 10:35:01 -07:00
Alexander Holliday ff28e74aca Merge pull request #3797 from LvckyAPI/fix-incorrect-status
Fix monitoring status stuck
2026-07-30 09:44:15 -07:00
Alexander Holliday 23b420d072 Merge pull request #3815 from bluewave-labs/feat/stacked-timing-chart
feat: stacked timing chart
2026-07-29 12:42:50 -07:00
Alex Holliday 0172272dc3 update tests 2026-07-29 12:33:56 -07:00
Alex Holliday 3f088c70f8 universal ymax cap 2026-07-29 12:30:36 -07:00
Alex Holliday ed54fa4668 first byte to total repsonse 2026-07-29 12:30:18 -07:00
Alex Holliday 0e0cace676 merge charts 2026-07-29 11:45:49 -07:00
Alex Holliday 58f1ae7fe9 extract xtick 2026-07-29 11:04:11 -07:00
Alex Holliday e3075318b6 denormalize resposne times 2026-07-29 10:57:20 -07:00
Alex Holliday 81aab35961 conditionally render charts 2026-07-28 15:27:19 -07:00
Alex Holliday 4e495eed33 add chart colors to theme 2026-07-28 15:27:04 -07:00
Alex Holliday 061da5e7ef coerce values to 0 if null 2026-07-28 15:26:52 -07:00
Anju a29b7037cf test: cover monitor tag filtering 2026-07-29 03:40:11 +05:30
Alex Holliday c7662727e7 add area charts 2026-07-28 11:24:23 -07:00
Alex Holliday 13c921531c update types, add skeleton for stacked timing phase chart 2026-07-28 11:13:50 -07:00
Alex Holliday 0cf8cfa5dc add avg timing phases to details response 2026-07-28 11:13:35 -07:00
Alex Holliday 4182a964a6 minor version bump 2026-07-28 09:48:31 -07:00
Iven Schlenther 04f7ef252a Fix indentation in statusService.ts for isUnknownStatus assignment. 2026-07-27 15:07:00 +02:00
Iven Schlenther 3201653f19 Fix monitor status handling: replace isKnownStatus with isUnknownStatus for clarity and simplify initialization logic. 2026-07-27 15:03:43 +02:00
Iven Schlenther 0988a61c1d Resolve legacy monitor status issues: handle unknown statuses, fix stuck states from old boolean values, and add regression tests. Adjust BaseChart text color for theme compatibility. 2026-07-27 15:03:41 +02:00
Alexander Holliday 80ea09b864 Merge pull request #3801 from bluewave-labs/chore/cleanup
chore: cleanup
2026-07-24 12:30:08 -07:00
Alex Holliday e0228918fb fix tests 2026-07-24 12:28:45 -07:00
Alex Holliday 8369db5ca2 remove logger 2026-07-24 11:38:08 -07:00
Alex Holliday 49e8d26bfc remove legacy try/catch blocks 2026-07-24 11:34:54 -07:00
Alex Holliday c28be97fb0 update tests 2026-07-24 11:32:18 -07:00
Alex Holliday ed36db172a dedupe hardware stat definitions, fix tests, remove misleading null return types 2026-07-24 11:32:00 -07:00
Alex Holliday c021d04f53 extract swtich 2026-07-24 11:09:53 -07:00
Alex Holliday 86eeda3f0e replace error instances with AppError 2026-07-24 10:49:39 -07:00
Alex Holliday 25d81f4238 remove dead findByMonitorIdAndDateRange method 2026-07-24 10:43:56 -07:00
Alexander Holliday af8ee643e1 Merge pull request #3800 from bluewave-labs/chore/monitor-domain-refactor
extact monitor children deletion to helper
2026-07-23 11:25:12 -07:00
Alex Holliday 61e442493a extact monitor children deletion to helper 2026-07-23 10:49:59 -07:00
Alexander Holliday 9540084425 Merge pull request #3799 from bluewave-labs/chore/domain-refactoring
chore: domain refactoring
2026-07-23 10:30:33 -07:00
Alex Holliday f50ba1bf23 update tests 2026-07-23 10:26:01 -07:00
Alex Holliday b4e42bfd68 add type check script 2026-07-23 10:25:51 -07:00
Alex Holliday 3af00281c4 extract date range calculation for conisistency 2026-07-23 10:25:24 -07:00
Alex Holliday 4a90ab8e78 extract pagination 2026-07-23 09:36:06 -07:00
Alexander Holliday 7598cf3007 Merge pull request #3798 from bluewave-labs/chore/api-cleanup
chore: api cleanup
2026-07-22 11:03:07 -07:00
Alex Holliday e1c89cd093 dedupe normalization 2026-07-22 11:00:59 -07:00
Alex Holliday 80d4e818f1 remove unused static vars 2026-07-22 10:36:16 -07:00
Alex Holliday 4949acac1c remove unused email templates, remove duplicate constants 2026-07-22 10:26:00 -07:00
Alex Holliday 586e79efb2 remove unused getters 2026-07-22 10:13:37 -07:00
Alex Holliday c961f5292d fix signature, add tests 2026-07-22 10:05:36 -07:00
Alex Holliday d53785b768 remove unused utilities, fix delete profile image issue 2026-07-22 10:03:48 -07:00
Alexander Holliday b9cbf199e6 Merge pull request #3796 from LvckyAPI/develop
Fixes UI bug with German translations
2026-07-22 09:44:19 -07:00
Iven Schlenther ac6678a986 Update English and German translations for "temp" label to "CPU temperature" in locales files. 2026-07-22 11:33:06 +02:00
Iven Schlenther ed1ef56184 - Add sx={{ overflowWrap: "anywhere" }} to Typography in BaseChart for improved text wrapping.
- Adjust `alignItems` to `stretch` and add `maxWidth` (260) to Gauge components in Infrastructure Details page.
- Update German translations for memory titles, temperatures, and labels in locales file.
2026-07-22 11:27:38 +02:00
Alexander Holliday ac16126e2e Merge pull request #3795 from bluewave-labs/fix/enums
fix: remove inline enum definitions
2026-07-21 10:53:52 -07:00
Alex Holliday 8e600e9a87 remove inline enum definitions 2026-07-21 10:48:18 -07:00
Alexander Holliday 790d83b7ed Merge pull request #3794 from bluewave-labs/fix/positional-args
fix: positional args
2026-07-21 10:30:29 -07:00
Alex Holliday c567e4be5e Update tests 2026-07-21 10:26:10 -07:00
Alex Holliday 936d9cd9a0 update call site 2026-07-21 10:26:02 -07:00
Alex Holliday 6e047d2cdb convert positional args to config obj 2026-07-21 10:25:50 -07:00
Alex Holliday aad03b787f update tests 2026-07-21 10:07:18 -07:00
Alex Holliday bb374db463 remove stray import 2026-07-21 10:07:09 -07:00
Alex Holliday 57e008c7ba update services config 2026-07-21 10:05:17 -07:00
Alex Holliday 5d3dc03c89 add a provider registry to notificaiton service, replace positional args and switch statements 2026-07-21 10:02:20 -07:00
Alexander Holliday bf255a0178 Merge pull request #3793 from bluewave-labs/chore/refactor-routes
chore: convert router classes to exported functions
2026-07-20 11:30:41 -07:00
Alex Holliday 36434c1160 convert router classes to exported functions 2026-07-20 11:25:39 -07:00
Alexander Holliday 63cecf3447 Merge pull request #3792 from bluewave-labs/chore/purge-team/groups
purge unused groups route, update tests
2026-07-20 10:26:45 -07:00
Alex Holliday 4d9e5821a8 purge unused groups route, update tests 2026-07-20 10:21:43 -07:00
Alexander Holliday 5433a0cf5f Merge pull request #3791 from bluewave-labs/chore/fix-access-modifier
chore: protected -> private as no class inherits
2026-07-20 10:06:08 -07:00
Alex Holliday d485adbb54 narrow mapDocument types 2026-07-20 10:04:55 -07:00
Alex Holliday 3eadb99c5e protected -> private as no class inherits 2026-07-20 10:01:48 -07:00
Alexander Holliday 9c04061a04 Merge pull request #3787 from jpomykala/develop
Add Polish language
2026-07-20 09:34:54 -07:00
Alexander Holliday 897ffd5736 Merge pull request #3788 from Osamaali313/fix/response-time-poor-label
Fix missing "Poor" label on the average response-time gauge
2026-07-20 09:34:16 -07:00
Osamaali313 368595178d Fix missing "Poor" label on the average response-time gauge
`RadialAvgResponse` maps the response-time palette to a qualitative word:

    const palette = getResponseTimeColor(avg);
    const msg: Record<string, string> = {
        success: "Excellent",
        warning: "Average",
        danger: "Poor",
    };
    ...
    {msg[palette]}

But `getResponseTimeColor` only ever returns `"success" | "warning" | "error"`
(MonitorUtils.ts) — never `"danger"`. So for the slow case `palette` is
`"error"`, `msg["error"]` is `undefined`, and no label renders. The same
component uses `theme.palette[palette].main`, which works precisely because
`palette` is the real MUI key `"error"`; every other palette helper in
MonitorUtils.ts also uses `"error"`, and `"danger"` appears nowhere else in
the client. Any monitor whose average response time is >= 300 ms shows an
empty label exactly where the "Poor" signal matters most. Use the `error`
key.
2026-07-18 23:36:43 +03:00
Jakub Pomykała 924c862244 Format pl.json 2026-07-18 22:13:05 +02:00
Jakub Pomykała cf451416b1 Update polish translations after review 2026-07-18 21:33:10 +02:00
Jakub Pomykała 6e2a64fc9f Add Polish to language selector 2026-07-18 20:57:50 +02:00
Jakub Pomykała 0e52cb6231 Add polish 2026-07-18 20:22:00 +02:00
Alexander Holliday 507b96164f Merge pull request #3784 from bluewave-labs/chore/refactor-utils
chore: refactor utils
2026-07-15 10:24:51 -07:00
Alex Holliday 887521c376 Merge branch 'develop' into chore/refactor-utils 2026-07-15 10:24:23 -07:00
Alexander Holliday caee9510eb Merge pull request #3783 from bluewave-labs/chore/controller-refactor
chore: add catchAsync to handle errors, remove all try/catch blocks from con…
2026-07-15 10:23:48 -07:00
Alex Holliday 825a81d326 extract shared mongo mapper utils 2026-07-15 10:20:16 -07:00
Alex Holliday bd13e9d9c8 add catchAsync to handle errors, remove all try/catch blocks from contorllers 2026-07-15 10:02:36 -07:00
Alexander Holliday 92696d3685 Merge pull request #3782 from bluewave-labs/chore/hardware-aggregation-refactor
chore: hardware aggregation refactor
2026-07-15 09:59:03 -07:00
Alex Holliday 5120390923 aggregation->aggregations 2026-07-15 09:23:16 -07:00
Alex Holliday 0810fcbb88 refactor hardware aggregation pipeline 2026-07-15 09:18:35 -07:00
Alexander Holliday 15453c2d8d Merge pull request #3781 from bluewave-labs/chore/monitor-service-refactor
chore: remove duplicated code
2026-07-14 10:48:53 -07:00
Alex Holliday 70f10a73d8 remove duplicated code 2026-07-14 10:46:25 -07:00
Alexander Holliday 930c06f5ff Merge pull request #3780 from bluewave-labs/chore/dedupe
chore: remove toEntityWithChecks
2026-07-14 10:37:14 -07:00
Alex Holliday b088a8c878 remove toEntityWithChecks 2026-07-14 10:17:38 -07:00
Alexander Holliday 067a186765 Merge pull request #3779 from bluewave-labs/fix/cascade-incidents
fix: delete incidents on all monitors deleted
2026-07-14 09:26:13 -07:00
Alex Holliday 107b5cbd96 delete incidents on all monitors deleted 2026-07-14 09:24:14 -07:00
Alexander Holliday 7fdd51c072 Merge pull request #3778 from edobusy/fix/readme-k8s-install-link
fix(docs): correct K8s install link path in translated READMEs
2026-07-14 09:10:10 -07:00
Alexander Holliday 2331035da0 Merge pull request #3773 from bluewave-labs/fix/status-page-brand-color-not-applied
fix(status page): apply brand color via a dedicated brand token
2026-07-14 09:04:06 -07:00
edobusy fd4edb7227 fix(docs): correct K8s install link path in translated READMEs
The translated READMEs in docs/translations/ link to the Kubernetes
installation guide with a path copied from the root README
(./charts/helm/checkmate/INSTALLATION.md). Because these files live two
levels below the repo root, that path resolves to
docs/translations/charts/..., which does not exist, so the link is
broken in all 17 translations.

Fix the path to ../../charts/helm/checkmate/INSTALLATION.md so it
resolves to the repo-root charts/ directory. The root README.md is
correct as-is and is left unchanged.
2026-07-14 13:43:37 +10:00
Alexander Holliday 538b0df7e9 Merge pull request #3777 from bluewave-labs/fix/shield-csp
add img.shields.io to csp directive
2026-07-13 10:11:24 -07:00
Alex Holliday 9f79feec8e add img.shields.io to csp directive 2026-07-13 10:10:28 -07:00
Alexander Holliday fe703199f5 Merge pull request #3776 from bluewave-labs/chore/simplify-deployment
chore: simplify deployment
2026-07-13 09:57:13 -07:00
Alex Holliday 800080fd06 add log warnings about legacy env vars 2026-07-13 09:48:02 -07:00
Alex Holliday c1bcd9b206 add note to helm chart about migration 2026-07-13 09:47:45 -07:00
Alex Holliday 5d55750b7b update docs 2026-07-13 09:38:55 -07:00
Alexander Holliday 4f398e98d3 Merge pull request #3774 from bluewave-labs/feat/italian-translation-and-contrib-docs
feat(i18n): add Italian translation, document translation workflow
2026-07-13 09:05:01 -07:00
gorkem-bwl 242e853bf0 i18n(it): review pass — fix grammar, awkward phrasing, inconsistencies
91 leaves refined after a native-Italian style pass.

## Categories of fixes

- Grammar / gender agreement — e.g. `logs.table.headers.lastSeenAt`
  "Ultima visto il" → "Visto l'ultima volta"; heap gauge subject
  agreement in `logs.diagnostics.gauges.allocatedPercentage`.
- Awkward phrasing / calques — `notFound.subtitle` drops stilted
  "ad esso"; `incidents.fallback.title` "in salute" → "funzionano
  correttamente" (was reading as if for a person, not systems);
  `settings.form.removeMonitors.title` "Reset del sistema" →
  "Reimposta il sistema"; "con successo" → "correttamente" on
  status-page delete confirmations.
- Consistency — paused monitor status unified on "In pausa" across
  `common.labels.paused`, monitor status labels, status-bar keys, and
  the `bulkPause.*_one/other` copies. Button verbs
  "Sospendi"/"Riprendi" kept for brevity; status noun stays "In pausa".
- Mistranslations — `buttons.testNotifications` "Testa" → "Prova";
  `buttons.toggleTheme` "Cambia" → "Alterna"; Twilio "Auth token" now
  translated; Mongoose `readyState` clarified to "Stato di connessione".
- Register — trimmed "Ancora" from several empty-state titles so they
  read like natural Italian empty states.

## Verification

874 leaves in EN and IT, 0 missing / 0 extra, all placeholders and
inline tags preserved, no empty values.
2026-07-13 16:18:24 +03:00
gorkem-bwl 523bf2253c feat(i18n): add Italian translation, document translation workflow
## Changes

- Add `client/src/locales/it.json` — Italian translation of all 874 UI
  keys, structure mirrors `en.json` exactly. Placeholders (`{{value}}`,
  etc.) and inline tag tokens (`<settingsLink>`, `<highlight>`, ...)
  are preserved. Product/brand names, technical abbreviations, and
  placeholder-style examples (e.g. `you@example.com`) are kept in
  English. 116 leaves are intentionally kept verbatim in English
  (brand names, protocol names, sample values, port numbers, regex).
- Register `it: "Italiano"` in `LanguageSelector`'s `languageNames`
  map so the dropdown shows the localised name; the selector already
  picks up any locale present in the i18n resources.
- Fill in the two "How do I..." sections that CONTRIBUTING.md's table
  of contents already links to but did not have bodies:
  - "Improve the documentation?" — short guidance on doc PRs and
    keeping README translations in sync.
  - "Help with translations?" — describes the current per-language
    JSON workflow (source of truth: `en.json`; improve an existing
    language by editing its JSON; add a new one by adding a JSON file
    and registering the display name in `LanguageSelector`). Notes
    that the older PoEditor sync workflow is no longer authoritative.

## Benefits

- Italian speakers can now use Checkmate in Italian.
- Contributors who follow the "Help with translations?" link from
  the CONTRIBUTING TOC land on real, current guidance instead of a
  broken anchor.
- Translation quality can be improved incrementally by anyone
  editing `it.json` directly; no external tooling required.
2026-07-13 16:10:04 +03:00
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
Alex Holliday ceacaa5499 scope workflows 2026-07-09 11:24:35 -07:00
Alex Holliday 91343f996b hosting docs 2026-07-09 10:55:04 -07:00
Alex Holliday 312961e8c9 use repo owner 2026-07-09 10:27:56 -07:00
Alex Holliday 0475f194e9 temporary branch 2026-07-09 10:24:06 -07:00
Alex Holliday bbb051406f udpate docs 2026-07-09 10:21:09 -07:00
Alex Holliday 40a7599388 update workflows 2026-07-09 10:20:24 -07:00
Alex Holliday dffce2709e clean up docker files 2026-07-09 10:19:38 -07:00
Alex Holliday 62031a0ab1 add a CLIENT_CONFIG to the client, serve from BE with env vars 2026-07-09 09:49:45 -07:00
Alex Holliday ef175950b2 add dockerfile and build-image workflow 2026-07-09 09:29:31 -07:00
Alex Holliday 6d91efecad default client to same-origin /api/v1 and window.location.origin, drop UPTIME_APP_* placeholders 2026-07-08 11:04:47 -07:00
Alex Holliday f7d3bcb696 add root docker-compose 2026-07-08 10:47:03 -07:00
Alex Holliday 4a093e2b97 start health server on primary process true 2026-07-08 10:46:34 -07: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
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
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
701 changed files with 54772 additions and 25470 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
**/node_modules
**/dist
**/.env
**/*.env
.git
docker/dev/mongo/data
+103
View File
@@ -0,0 +1,103 @@
name: Build image
on:
push:
branches: ["develop", "master"]
tags: ["v*"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# checkmate-backend-mono is a transitional alias so existing deployments
# keep receiving updates; drop it after one release cycle.
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/checkmate
ghcr.io/${{ github.repository_owner }}/checkmate-backend-mono
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# The hosted staging/prod instances share one host and one compose file; each
# deploy pulls and recreates only its own service.
deploy-staging:
needs: build-and-push
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy staging.checkmate.so
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docs/hosting/checkmate-so
docker compose pull checkmate-staging
docker compose up -d checkmate-staging
docker system prune -af
deploy-prod:
needs: build-and-push
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Deploy demo.checkmate.so
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docs/hosting/checkmate-so
docker compose pull checkmate-demo
docker compose up -d checkmate-demo
docker system prune -af
@@ -1,148 +0,0 @@
name: Deploy images on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }}
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
no-cache: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }}
-143
View File
@@ -1,143 +0,0 @@
name: Deploy images
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:latest \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:latest
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:latest \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:latest
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:latest \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:latest
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
no-cache: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ env.VERSION }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest
-91
View File
@@ -1,91 +0,0 @@
name: Demo deploy
on:
push:
branches: [ "demo" ]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-demo
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-demo \
-f ./docker/prod/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-demo
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-demo \
-f ./docker/prod/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo
deploy-to-demo:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
steps:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docker/unified
docker compose down
docker compose pull
docker compose up -d
docker system prune -af
-91
View File
@@ -1,91 +0,0 @@
name: Staging deploy
on:
push:
branches: [ "develop" ]
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
-f ./docker/staging/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-staging
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-staging \
-f ./docker/staging/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-staging
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-staging \
-f ./docker/staging/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging
deploy-to-staging:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
steps:
- name: SSH into server and restart container using Docker Compose
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.DEMO_SERVER_HOST }}
username: ${{ secrets.DEMO_SERVER_USER }}
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
script: |
cd checkmate
git pull
cd docker/unified
docker compose down
docker compose pull
docker compose up -d
docker system prune -af
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
+33 -17
View File
@@ -35,9 +35,9 @@ npm run format # Prettier formatting
### Docker Development
```bash
cd docker/dev
./build_images.sh
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
docker build -f docker/Dockerfile -t checkmate . # build the mono image (server + built client)
docker compose -f docker/dev/docker-compose.yaml up # local full stack (builds image, runs mongo + worker)
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0 # just a dev database
```
## Environment Setup
@@ -68,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
@@ -159,14 +163,26 @@ When working on anything related to check scheduling, incident lifecycle, or not
## Code Conventions
### Frontend conventions (mandatory for `client/src`)
Read `docs/frontend-conventions.md` before touching any `.tsx` file. Five rules, all enforced in code review:
### 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
+31 -25
View File
@@ -88,34 +88,10 @@ cd Checkmate
#### Step 2: Set Up Docker Containers (MongoDB)
Navigate to the Docker dev directory:
```bash
cd docker/dev
```
Build the Docker images:
Linux:
```bash
./build_images.sh
```
Windows:
```bash
./build_images.bat
```
Run MongoDB container:
```bash
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
```
Navigate back to the root directory:
```bash
cd ../..
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0
```
#### Step 3: Set Up the Backend (Server)
@@ -151,6 +127,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:
@@ -255,6 +232,35 @@ 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?
Documentation contributions are very welcome — fixing a typo, clarifying a step, or adding a missing example is a valuable PR.
- Small edits (typos, wording, formatting): open a PR directly against `develop`.
- Larger changes (restructuring a section, adding a new guide): open an issue first so we can align on scope, then a PR.
- Screenshots are helpful when documenting UI behaviour.
- README translations live in `docs/translations/`. If you're adding a new language, please also add a flag + link to the language bar in the root `README.md` and every existing translated README so they stay in sync.
### Help with translations?
The application uses `i18next` with one JSON file per language under `client/src/locales/` (`en.json` is the source of truth). English source strings are the definitive copy; other languages are generated as one-time machine translations that anyone is welcome to refine.
**To improve an existing language:**
1. Edit the relevant `client/src/locales/<code>.json` file directly.
2. Keep the JSON key structure identical to `en.json` — only translate the string values.
3. Open a PR against `develop`. No language-selector code changes are needed.
**To add a new language:**
1. Add `client/src/locales/<code>.json` with the same key structure as `en.json`, translated. If you can't translate every key, leave unfamiliar strings in English rather than removing keys — i18next falls back to the key path when a value is missing.
2. Add the display name to `languageNames` in `client/src/Components/inputs/LanguageSelector.tsx` (e.g. `it: "Italiano"`). The selector picks up any language present in `resources` automatically.
3. Open a PR against `develop`.
If translating a whole file is more than you want to take on, open an issue asking us to add the language and we'll generate an initial machine-translated file that you (or others) can refine over time.
**Note:** an older workflow that synced translations with PoEditor is still in the repo but no longer authoritative. Please treat the `.json` files in `client/src/locales/` as the source of truth.
### Submit a pull request?
Follow the [pull request checklist](#pull-request-checklist). Your PR should:
-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://checkmate.so/docs). 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://checkmate.so/docs/getting-started/installation).
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://deepwiki.com/bluewave-labs/Checkmate#system-architecture) y la [visión general](https://deepwiki.com/bluewave-labs/Checkmate#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.
+73 -24
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,9 +77,42 @@ 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).
The quickest way to run Checkmate is the reference Docker Compose file. It starts two services: the all-in-one Checkmate application image (`ghcr.io/bluewave-labs/checkmate`) and a separate MongoDB service.
> **What “all-in-one” means:** the Checkmate application is packaged in a single image; MongoDB is not embedded in that image and remains required. The reference Compose file starts MongoDB for you. For custom deployments, configure `DB_CONNECTION_STRING` to use an external MongoDB instance.
```bash
curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.yaml
JWT_SECRET="$(openssl rand -hex 32)" docker compose up -d
```
Then open http://localhost:52345. If the app is reached at another origin (domain or LAN IP), set `CLIENT_HOST` accordingly. To build the image yourself, run `docker build -f docker/Dockerfile -t checkmate .` from a checkout. For TLS, put any reverse proxy (Caddy, Traefik, nginx) in front of port 52345.
### Configuration
The image is configured entirely through environment variables on the server container:
| Variable | Required | Description |
|---|---|---|
| `DB_CONNECTION_STRING` | Yes | MongoDB connection string, e.g. `mongodb://mongodb:27017/uptime_db` |
| `JWT_SECRET` | Yes | Secret used to sign auth tokens; generate one with `openssl rand -hex 32` |
| `CLIENT_HOST` | Yes | The URL users reach the app at, e.g. `https://checkmate.example.com`; used for CORS and for links in notifications and emails |
| `LOG_LEVEL` | No | Server log level: `error`, `warn`, `info`, or `debug` (default `debug`) |
The web client needs no configuration by default: it calls the API on the same origin it was served from (`/api/v1`). For setups where the defaults don't apply — for example, the API is reached through a different origin than the page — the server renders overrides into the client at runtime via these optional variables:
| Variable | Description |
|---|---|
| `CLIENT_CONFIG_API_BASE_URL` | Full base URL the client calls the API at, e.g. `https://api.example.com/api/v1`; defaults to same-origin `/api/v1` |
| `CLIENT_CONFIG_CLIENT_HOST` | Origin used when the client builds absolute links (invites, status pages); defaults to the browser's current origin |
| `CLIENT_CONFIG_LOG_LEVEL` | Browser console log level: `error`, `warn`, `info`, or `debug` (default `error`) |
> **Upgrading from an older image?** The `UPTIME_APP_*` variables (`UPTIME_APP_API_BASE_URL`, `UPTIME_APP_CLIENT_HOST`, `UPTIME_APP_LOG_LEVEL`) are no longer read. In most setups no replacement is needed — the same-origin defaults cover them; if you pointed the client at a different origin, use the `CLIENT_CONFIG_*` equivalents above. The `checkmate-client`, `checkmate-backend`, `checkmate-mongo`, and `checkmate-backend-mono-multiarch` images are no longer updated — switch to `ghcr.io/bluewave-labs/checkmate`, keeping your existing MongoDB service and data volume.
See full installation instructions in the [Checkmate documentation portal](https://checkmate.so/docs).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa), [Cloudzy](https://cloudzy.com/marketplace/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
@@ -67,6 +122,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 +133,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 +143,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, Rocket.Chat, 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 +168,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 +186,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 +196,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 +204,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.
+6 -2
View File
@@ -2,5 +2,9 @@ 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
# DO NOT bump appVersion past v3.8.1 until this chart is migrated to the all-in-one
# image (ghcr.io/bluewave-labs/checkmate). The checkmate-client / checkmate-backend /
# checkmate-mongo images this chart references are frozen at v3.8.1 — later versions
# are never published, so a bumped chart will fail to pull.
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
+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: ""
+6 -4
View File
@@ -1,4 +1,6 @@
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
VITE_APP_API_V2_BASE_URL=UPTIME_APP_API_V2_BASE_URL
VITE_APP_CLIENT_HOST=UPTIME_APP_CLIENT_HOST
VITE_APP_LOG_LEVEL=UPTIME_APP_LOG_LEVEL
# Production builds use same-origin defaults: API at /api/v1, client host from
# window.location.origin. The empty values below override any local .env so a
# production build never bakes in a developer's localhost URLs.
VITE_APP_API_BASE_URL=
VITE_APP_CLIENT_HOST=
VITE_APP_LOG_LEVEL=
-1
View File
@@ -28,5 +28,4 @@ dist-ssr
# Scripts
*.sh
!env.sh
.env.development
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
for i in $(env | grep UPTIME_APP_)
do
key=$(echo $i | cut -d '=' -f 1)
value=$(echo $i | cut -d '=' -f 2-)
echo $key=$value
find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
done
+1
View File
@@ -12,6 +12,7 @@
/>
<title>Checkmate</title>
<script src="/config.js"></script>
</head>
<body>
+568 -211
View File
File diff suppressed because it is too large. Load diff
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "3.7.1",
"version": "3.10.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -14,9 +14,10 @@
"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",
@@ -27,12 +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",
"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",
@@ -66,8 +67,9 @@
"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"
}
}
+4
View File
@@ -0,0 +1,4 @@
// Fallback so /config.js always resolves (e.g. under the Vite dev server). In
// the Docker image the server shadows this path and renders the real config
// from its CLIENT_CONFIG_* environment variables.
window.__CHECKMATE_CONFIG__ = {};
@@ -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,
}}
/>
@@ -1,7 +1,7 @@
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { formatDateWithTz, formatMs } from "@/Utils/TimeUtils";
import type { CheckSnapshot } from "@/Types/Check";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
@@ -58,7 +58,7 @@ export const HeatmapResponseTimeTooltip = ({
</Typography>
<Typography>
{t("common.labels.responseTime")}:{" "}
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
{check?.responseTime ? formatMs(check.responseTime) : "N/A"}
</Typography>
<Typography textTransform={"capitalize"}>
Status:{" "}
@@ -7,6 +7,9 @@ import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
import { computeBarHeights } from "@/Utils/DataUtils";
import { formatMs } from "@/Utils/TimeUtils";
interface HistogramResponseTimeProps {
checks: CheckSnapshot[];
@@ -17,25 +20,25 @@ interface HistogramResponseTimeProps {
}
interface ResponseTimeStats {
max: number | string;
avg: number | string;
max: string | null;
avg: string | null;
}
const DEFAULT_HEIGHT = 50;
const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => {
if (!Array.isArray(checks) || checks.length === 0) {
return { max: "-", avg: "-" };
return { max: null, avg: null };
}
const validChecks = checks.filter((check) => check.originalResponseTime != null);
const validChecks = checks.filter((check) => check.responseTime != null);
if (validChecks.length === 0) {
return { max: "-", avg: "-" };
return { max: null, avg: null };
}
const responseTimes = validChecks.map((check) => check.originalResponseTime);
const max = Math.max(...responseTimes);
const avg = Math.round(
const responseTimes = validChecks.map((check) => check.responseTime);
const max = formatMs(Math.max(...responseTimes));
const avg = formatMs(
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
);
@@ -53,78 +56,86 @@ export const HistogramResponseTime = ({
const { t } = useTranslation();
const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]);
const barHeights = useMemo(() => computeBarHeights(checks), [checks]);
if (!Array.isArray(checks) || checks.length === 0) return null;
const data =
checks.length !== 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;
const gridGap = gap ?? theme.spacing(0.5);
const statsContent = showStats &&
(typeof stats.max === "number" || typeof stats.avg === "number") && (
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
minWidth={70}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
const statsContent = showStats && (stats.max !== null || stats.avg !== null) && (
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
width={110}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
>
<Typography
variant="body2"
noWrap
>
<Typography variant="body2">
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography variant="body2">
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
);
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
);
return (
<Stack
direction={statsPosition === "left" ? "row-reverse" : "row"}
alignItems="center"
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) => {
const isPlaceholder = (check as any).status === "placeholder";
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
const heightPct = `${barHeights[index] ?? 0}%`;
const barColor =
check.status === true
? theme.palette.success.light
: theme.palette.error.light;
const bar = (
<Box
width={"100%"}
height={"100%"}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
position: "relative",
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 +144,8 @@ export const HistogramResponseTime = ({
return isPlaceholder ? (
<Box
height={"100%"}
key={index}
sx={{ height: "100%" }}
>
{bar}
</Box>
@@ -13,6 +13,7 @@ type BaseChartProps = React.PropsWithChildren<{
width?: number | string;
maxWidth?: number | string;
padding?: number | string | ResponsiveStyleValue<number | string>;
onClick?: () => void;
}>;
export const BaseChart = ({
@@ -21,16 +22,22 @@ export const BaseChart = ({
width = "100%",
maxWidth = "100%",
padding,
onClick,
}: BaseChartProps) => {
const theme = useTheme();
return (
<BaseBox
onClick={onClick}
sx={{
padding: padding ?? theme.spacing(LAYOUT.MD),
display: "flex",
flex: 1,
width: width,
maxWidth: maxWidth,
...(onClick && {
cursor: "pointer",
"& .recharts-wrapper": { cursor: "pointer !important" },
}),
}}
>
<Stack
@@ -40,6 +47,7 @@ export const BaseChart = ({
<Typography
variant="eyebrow"
color="text.secondary"
sx={{ overflowWrap: "anywhere" }}
>
{title}
</Typography>
+14 -4
View File
@@ -1,18 +1,28 @@
import type { CSSProperties } from "react";
export const DotSizes = ["xs", "sm", "md", "lg"] as const;
export type DotSize = (typeof DotSizes)[number];
const DOT_SIZE_PX: Record<DotSize, string> = {
xs: "2px",
sm: "4px",
md: "8px",
lg: "12px",
};
interface DotProps {
color?: string;
size?: string;
size?: DotSize;
style?: CSSProperties;
}
export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
export const Dot = ({ color = "gray", size = "sm", style }: DotProps) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
width: DOT_SIZE_PX[size],
height: DOT_SIZE_PX[size],
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
+21 -13
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,
@@ -5,13 +5,15 @@ interface IconProps {
size?: number;
strokeWidth?: number;
stroke?: string;
color?: string;
}
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5, color }: IconProps) => {
return (
<Icon
size={size}
strokeWidth={strokeWidth}
color={color}
/>
);
};
@@ -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";
import { formatDuration } from "@/Utils/TimeUtils";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -28,7 +29,7 @@ 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
@@ -41,11 +42,17 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
>
{formatUrl(monitor?.url)}
</Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && (
<>
<Dot />
<Typography>
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
Checking every {formatDuration(monitor?.interval, true)}
</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>
);
};
@@ -35,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;
};
@@ -109,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
@@ -123,7 +144,7 @@ export function DataTable<
component="div"
color={theme.palette.text.primary}
>
{header.content}
{header.mobileLabel ?? header.content}
</Typography>
</Grid2>
<Grid2
@@ -193,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}
@@ -222,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
@@ -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 (
@@ -27,6 +31,7 @@ export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) =>
boxShadow: theme.shadows[6],
},
},
...slotProps,
}}
>
{props.children}
@@ -25,3 +25,6 @@ export * from "./TextLink";
export * from "./NoticeBanner";
export * from "./OfflineBanner";
export * from "./Avatar";
export * from "./StatusCodeLabel";
export * from "./StrategyBadge";
export * from "./StepProgress";
+36 -20
View File
@@ -6,6 +6,7 @@ import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { FieldLabel } from "./FieldLabel";
import { ChevronDown } from "lucide-react";
import { useTranslation } from "react-i18next";
type AutoCompleteInputProps = Omit<
AutocompleteProps<any, boolean, boolean, boolean>,
@@ -14,27 +15,38 @@ type AutoCompleteInputProps = Omit<
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
fieldLabel?: string;
required?: boolean;
renderOptionContent?: (option: any) => React.ReactNode;
placeholder?: string;
error?: boolean;
helperText?: string;
};
export const AutoCompleteInput = ({
fieldLabel,
required,
renderInput,
renderOptionContent,
placeholder,
error,
helperText,
...props
}: AutoCompleteInputProps) => {
const theme = useTheme();
const { t } = useTranslation();
const multiple = props.multiple;
const { renderOption: customRenderOption, ...autocompleteProps } = props;
const defaultRenderInput = (params: any) => (
<TextField
{...params}
placeholder="Type to search"
placeholder={placeholder ?? t("common.form.autocomplete.placeholder")}
error={error}
helperText={helperText}
/>
);
const autocomplete = (
<Autocomplete
{...props}
{...autocompleteProps}
disableCloseOnSelect={!!multiple}
popupIcon={
<ChevronDown
@@ -46,24 +58,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)}`,
+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" },
@@ -10,9 +10,12 @@ import type { RootState } from "@/Types/state";
export const languageNames: Record<string, string> = {
en: "English",
de: "Deutsch",
ca: "Català",
es: "Español",
fr: "Français",
it: "Italiano",
"pt-BR": "Português (BR)",
pl: "Polski",
cs: "Čeština",
fi: "Suomi",
tr: "Türkçe",
+3
View File
@@ -42,6 +42,9 @@ export const RadioWithDescription = ({
"& .MuiButtonBase-root": {
mr: theme.spacing(6),
},
"&:has(.Mui-focusVisible)": {
backgroundColor: theme.palette.action.hover,
},
}}
/>
);
+1 -1
View File
@@ -22,7 +22,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
width: "100%",
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: 34,
height: props.multiline ? "auto" : 34,
fontSize: typographyLevels.base,
overflow: "hidden",
},
+21 -12
View File
@@ -3,6 +3,7 @@ import type { MobileTimePickerProps } from "@mui/x-date-pickers/MobileTimePicker
import type { Dayjs } from "dayjs";
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { FieldLabel } from "./FieldLabel";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
@@ -10,11 +11,15 @@ import { LocalizationProvider } from "@mui/x-date-pickers";
interface TimePickerComponentProps extends Omit<MobileTimePickerProps<Dayjs>, "label"> {
fieldLabel?: string;
required?: boolean;
error?: boolean;
helperText?: string;
}
export const TimePickerComponent = ({
fieldLabel,
required,
error,
helperText,
...props
}: TimePickerComponentProps) => {
const theme = useTheme();
@@ -33,12 +38,12 @@ export const TimePickerComponent = ({
px: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.divider,
borderColor: error ? theme.palette.error.main : theme.palette.divider,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.divider,
borderColor: error ? theme.palette.error.main : theme.palette.divider,
},
},
},
@@ -48,14 +53,18 @@ export const TimePickerComponent = ({
</LocalizationProvider>
);
if (fieldLabel) {
return (
<Stack spacing={theme.spacing(2)}>
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
{picker}
</Stack>
);
}
return picker;
return (
<Stack spacing={theme.spacing(2)}>
{fieldLabel && <FieldLabel required={required}>{fieldLabel}</FieldLabel>}
{picker}
{helperText && (
<Typography
variant="caption"
color={error ? "error" : "text.secondary"}
>
{helperText}
</Typography>
)}
</Stack>
);
};
@@ -0,0 +1,52 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { Autocomplete } from "@/Components/inputs";
export interface AutocompleteOption {
id: string;
name: string;
}
interface FormAutocompleteProps<T extends FieldValues, O extends AutocompleteOption> {
name: FieldPath<T>;
options: O[];
fieldLabel?: string;
placeholder?: string;
}
export const FormAutocompleteField = <
T extends FieldValues,
O extends AutocompleteOption,
>({
name,
options,
fieldLabel,
placeholder,
}: FormAutocompleteProps<T, O>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<Autocomplete
options={options}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, value) => option.id === value.id}
value={options.find((o) => o.id === field.value) ?? null}
onChange={(_: unknown, newValue: O | null) => {
field.onChange(newValue?.id ?? "");
}}
fieldLabel={fieldLabel}
placeholder={placeholder}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
);
};
@@ -0,0 +1,38 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import FormControlLabel from "@mui/material/FormControlLabel";
import { Checkbox } from "@/Components/inputs";
interface FormCheckboxFieldProps<T extends FieldValues> {
name: FieldPath<T>;
label: string;
}
export const FormCheckboxField = <T extends FieldValues>({
name,
label,
}: FormCheckboxFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
label={label}
/>
)}
/>
);
};
@@ -0,0 +1,35 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { ColorInput } from "@/Components/inputs";
interface FormColorFieldProps<T extends FieldValues> {
name: FieldPath<T>;
fieldLabel?: string;
}
export const FormColorField = <T extends FieldValues>({
name,
fieldLabel,
}: FormColorFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={fieldLabel}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
);
};
@@ -0,0 +1,81 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import dayjs, { type Dayjs } from "dayjs";
import { DatePicker, TimePicker } from "@/Components/inputs";
export interface FormPickerFieldProps<T extends FieldValues> {
name: FieldPath<T>;
fieldLabel?: string;
format?: string;
onValueChange?: (value: string) => void; // e.g. () => trigger("startDate")
}
interface DayjsPickerProps {
fieldLabel?: string;
value: Dayjs | null;
onChange: (value: Dayjs | null) => void;
error?: boolean;
helperText?: string;
}
interface FormDayjsPickerFieldProps<
T extends FieldValues,
> extends FormPickerFieldProps<T> {
picker: React.ComponentType<DayjsPickerProps>;
format: string;
}
const FormDayjsPickerField = <T extends FieldValues>({
name,
fieldLabel,
format,
onValueChange,
picker: Picker,
}: FormDayjsPickerFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<Picker
fieldLabel={fieldLabel}
value={field.value ? dayjs(field.value, format) : null}
onChange={(value) => {
const next = value ? value.format(format) : "";
field.onChange(next);
onValueChange?.(next);
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
);
};
export const FormDatePickerField = <T extends FieldValues>({
format = "YYYY-MM-DD",
...rest
}: FormPickerFieldProps<T>) => (
<FormDayjsPickerField
{...rest}
format={format}
picker={DatePicker}
/>
);
export const FormTimePickerField = <T extends FieldValues>({
format = "HH:mm",
...rest
}: FormPickerFieldProps<T>) => (
<FormDayjsPickerField
{...rest}
format={format}
picker={TimePicker}
/>
);
@@ -0,0 +1,187 @@
import { useMemo } from "react";
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import { useTheme } from "@mui/material/styles";
import { Trash2, GripVertical } from "lucide-react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
import type { DropResult } from "@hello-pangea/dnd";
import { useTranslation } from "react-i18next";
import { Autocomplete } from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
export interface MultiSelectOption {
id: string | number;
name: string;
}
interface FormMultiSelectFieldProps<T extends FieldValues, O extends MultiSelectOption> {
name: FieldPath<T>;
options: O[];
fieldLabel?: string;
placeholder?: string;
description?: string;
renderRow?: (option: O) => React.ReactNode;
renderOptionContent?: (option: O) => React.ReactNode;
sortable?: boolean;
}
export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelectOption>({
name,
options,
fieldLabel,
placeholder,
description,
renderRow,
renderOptionContent,
sortable = false,
}: FormMultiSelectFieldProps<T, O>) => {
const { control } = useFormContext<T>();
const theme = useTheme();
const { t } = useTranslation();
const optionsById = useMemo(
() => new Map<O["id"], O>(options.map((o) => [o.id, o])),
[options]
);
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => {
const selectedIds: O["id"][] = field.value ?? [];
const selected = selectedIds
.map((id) => optionsById.get(id))
.filter((o): o is O => o !== undefined);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const reordered = selected.map((o) => o.id);
const [moved] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, moved);
const unresolved = selectedIds.filter((id) => !optionsById.has(id));
field.onChange([...reordered, ...unresolved]);
};
const rowContent = (option: O) =>
renderRow ? (
renderRow(option)
) : (
<Typography flexGrow={1}>{option.name}</Typography>
);
const removeButton = (option: O) => (
<IconButton
size="small"
onClick={() => {
field.onChange(selectedIds.filter((id) => id !== option.id));
}}
aria-label={t("common.buttons.removeItem", { item: option.name })}
>
<Trash2 size={16} />
</IconButton>
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
autoHighlight
options={options}
value={selected}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, value) => option.id === value.id}
onChange={(_: unknown, newValue: O[]) => {
// The Autocomplete only sees resolved options, so ids missing
// from options must be re-appended or they'd be silently
// dropped (e.g. a role excluded from the selectable list).
const unresolved = selectedIds.filter((id) => !optionsById.has(id));
field.onChange([...newValue.map((o) => o.id), ...unresolved]);
}}
fieldLabel={fieldLabel}
placeholder={selected.length > 0 ? "" : placeholder}
renderOptionContent={renderOptionContent}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
{description && (
<Typography
component="span"
color={theme.palette.text.secondary}
sx={{ opacity: 0.8 }}
>
{description}
</Typography>
)}
{sortable
? selected.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId={`${name}-list`}>
{(provided) => (
<Stack
{...provided.droppableProps}
ref={provided.innerRef}
>
{selected.map((option, index) => (
<Draggable
key={option.id}
draggableId={String(option.id)}
index={index}
>
{(provided) => (
<Stack
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
direction="row"
alignItems="center"
spacing={theme.spacing(LAYOUT.XS)}
padding={theme.spacing(LAYOUT.XS)}
marginTop={theme.spacing(SPACING.LG)}
borderRadius={1}
border={`1px solid ${theme.palette.divider}`}
sx={{
cursor: "grab",
"&:active": { cursor: "grabbing" },
}}
>
<GripVertical size={20} />
{rowContent(option)}
{removeButton(option)}
</Stack>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</DragDropContext>
)
: selected.map((option) => (
<Stack
key={option.id}
direction="row"
alignItems="center"
spacing={theme.spacing(LAYOUT.XS)}
padding={theme.spacing(LAYOUT.XS)}
marginTop={theme.spacing(SPACING.LG)}
borderRadius={1}
border={`1px solid ${theme.palette.divider}`}
width="100%"
>
{rowContent(option)}
{removeButton(option)}
</Stack>
))}
</Stack>
);
}}
/>
);
};
@@ -0,0 +1,46 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { TextField } from "@/Components/inputs";
interface FormNumberFieldProps<T extends FieldValues> extends Omit<
React.ComponentProps<typeof TextField>,
"name" | "error" | "helperText" | "type"
> {
name: FieldPath<T>;
emptyValue?: number; // what an empty input writes to the form; 0 keeps today's sentinel
}
export const FormNumberField = <T extends FieldValues>({
name,
emptyValue = 0,
...rest
}: FormNumberFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="number"
value={
field.value === undefined || field.value === emptyValue ? "" : field.value
}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === "" ? emptyValue : Number(val));
}}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
{...rest}
/>
)}
/>
);
};
@@ -0,0 +1,50 @@
import { RadioGroup, useTheme } from "@mui/material";
import FormControl from "@mui/material/FormControl";
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { LAYOUT } from "@/Utils/Theme/constants";
import { RadioWithDescription } from "@/Components/inputs/Radio";
export interface FormRadioOption {
value: string;
label: string;
description: string;
}
interface FormRadioGroupProps<T extends FieldValues> {
name: FieldPath<T>;
options: FormRadioOption[];
}
export const FormRadioGroup = <T extends FieldValues>({
name,
options,
}: FormRadioGroupProps<T>) => {
const { control } = useFormContext<T>();
const theme = useTheme();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<FormControl error={!!fieldState.error}>
<RadioGroup
{...field}
sx={{ gap: theme.spacing(LAYOUT.MD) }}
>
{options.map((option) => (
<RadioWithDescription
key={option.value}
value={option.value}
label={option.label}
description={option.description}
/>
))}
</RadioGroup>
</FormControl>
)}
/>
);
};
@@ -0,0 +1,60 @@
import {
Controller,
useFormContext,
type FieldValues,
type FieldPath,
} from "react-hook-form";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/Components/inputs";
export interface FormSelectOption {
value: string | number;
label: string;
}
interface FormSelectProps<T extends FieldValues> extends Omit<
React.ComponentProps<typeof Select>,
"name" | "error" | "value" | "onChange"
> {
name: FieldPath<T>;
options: FormSelectOption[];
fallbackValue?: string | number; // display value while the field is undefined
onValueChange?: (value: string | number) => void; // side effects, see Notes
}
export const FormSelectField = <T extends FieldValues>({
name,
options,
fallbackValue,
onValueChange,
...rest
}: FormSelectProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? fallbackValue ?? ""}
onChange={(e) => {
field.onChange(e.target.value);
onValueChange?.(e.target.value as string | number);
}}
error={!!fieldState.error}
{...rest}
>
{options.map((option) => (
<MenuItem
key={option.value}
value={option.value}
>
{option.label}
</MenuItem>
))}
</Select>
)}
/>
);
};
@@ -0,0 +1,34 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { SliderWithLabel } from "@/Components/inputs";
interface FormSliderProps<T extends FieldValues> extends Omit<
React.ComponentProps<typeof SliderWithLabel>,
"name" | "value" | "onChange"
> {
name: FieldPath<T>;
}
export const FormSliderField = <T extends FieldValues>({
name,
...rest
}: FormSliderProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
{...rest}
/>
)}
/>
);
};
@@ -0,0 +1,59 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { SwitchComponent } from "@/Components/inputs";
import { SPACING } from "@/Utils/Theme/constants";
interface FormSwitchProps<T extends FieldValues> {
name: FieldPath<T>;
label: string;
labelPlacement?: "start" | "end"; // left | right
}
export const FormSwitchField = <T extends FieldValues>({
name,
label,
labelPlacement = "end",
}: FormSwitchProps<T>) => {
const { control } = useFormContext<T>();
const theme = useTheme();
return (
<Controller
name={name}
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={labelPlacement === "start" ? undefined : theme.spacing(SPACING.LG)}
justifyContent={labelPlacement === "start" ? "space-between" : undefined}
>
{labelPlacement === "start" ? (
<>
<Typography>{label}</Typography>
<SwitchComponent
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
</>
) : (
<>
<SwitchComponent
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>{label}</Typography>
</>
)}
</Stack>
)}
/>
);
};
@@ -0,0 +1,40 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { TextField } from "@/Components/inputs";
interface FormTextFieldProps<T extends FieldValues> extends Omit<
React.ComponentProps<typeof TextField>,
"name" | "error" | "helperText"
> {
name: FieldPath<T>;
helperText?: string;
}
export const FormTextField = <T extends FieldValues>({
name,
...rest
}: FormTextFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
{...rest}
value={field.value ?? ""}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? rest.helperText ?? ""}
fullWidth
/>
)}
/>
);
};
+7 -9
View File
@@ -18,15 +18,13 @@ const RootLayout = () => {
flex={1}
padding={6}
overflow={"hidden"}
sx={{
backgroundColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.01)"
: "rgba(0, 0, 0, 0.01)",
display: "flex",
alignItems: "center",
paddingLeft: isSmall ? `${collapsedWidth + 12}px` : 12,
}}
bgcolor={
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.01)"
: "rgba(124, 116, 116, 0.01)"
}
alignItems="center"
paddingLeft={isSmall ? `${collapsedWidth + 12}px` : 12}
>
<Stack
maxWidth={1280}
@@ -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,7 +3,10 @@ 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";
import { formatMs } from "@/Utils/TimeUtils";
interface GeoChecksMapProps {
geoChecks: FlatGeoCheck[];
@@ -14,6 +17,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
@@ -80,10 +84,6 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
return status ? theme.palette.success.main : theme.palette.error.main;
};
const formatResponseTime = (timing: number): string => {
return `${timing.toFixed(0)}ms`;
};
return (
<>
{mapPopupStyles}
@@ -174,7 +174,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
>
Status Code:
</Typography>{" "}
{selectedCheck.statusCode}
{formatStatusCode(selectedCheck.statusCode, t)}
</Typography>
<Typography variant="body2">
<Typography
@@ -183,7 +183,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
>
Response Time:
</Typography>{" "}
{formatResponseTime(selectedCheck.timings.total)}
{formatMs(selectedCheck.timings.total)}
</Typography>
<Typography
variant="caption"
@@ -1,22 +1,24 @@
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/design-elements";
import prettyMilliseconds from "pretty-ms";
import { useTheme } from "@mui/material/styles";
import type { MonitorStats, Monitor } from "@/Types/Monitor";
import { getStatusPalette } from "@/Utils/MonitorUtils";
import { useTranslation } from "react-i18next";
import { formatMs, formatDuration } from "@/Utils/TimeUtils";
interface MonitorStatBoxesProps {
monitor?: Monitor;
monitorStats: MonitorStats | null;
certificateExpiry?: string;
domainExpiry?: string;
}
export const MonitorStatBoxes = ({
monitor,
monitorStats,
certificateExpiry,
domainExpiry,
}: MonitorStatBoxesProps) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -31,14 +33,9 @@ export const MonitorStatBoxes = ({
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
const timeSinceLastCheck = Date.now() - timeOfLastCheck || 0;
const options = {
secondsDecimalDigits: 0,
millisecondsDecimalDigits: 0,
};
const streakTime = formatDuration(timeSinceLastFailure);
const streakTime = prettyMilliseconds(timeSinceLastFailure, options);
const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options);
const lastCheckTime = formatDuration(timeSinceLastCheck);
const isActive =
monitor?.status === "up" ||
monitor?.status === "paused" ||
@@ -67,14 +64,20 @@ export const MonitorStatBoxes = ({
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastResponseTime")}
subtitle={prettyMilliseconds(monitorStats?.lastResponseTime ?? 0)}
subtitle={formatMs(monitorStats?.lastResponseTime ?? 0)}
/>
{monitor?.type === "http" && (
<StatBox
title={t("pages.common.monitors.statBoxes.certificateExpiry")}
subtitle={certificateExpiry || "N/A"}
/>
<>
<StatBox
title={t("pages.common.monitors.statBoxes.certificateExpiry")}
subtitle={certificateExpiry || "N/A"}
/>
<StatBox
title={t("pages.common.monitors.statBoxes.domainExpiry")}
subtitle={domainExpiry || "N/A"}
/>
</>
)}
</Stack>
);
@@ -0,0 +1,34 @@
export const computeYAxisCap = (values: number[]): number | undefined => {
const sorted = values.slice().sort((a, b) => a - b);
if (sorted.length < 2) return undefined;
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
};
export const PHASE_KEYS = [
"avgDns",
"avgTcp",
"avgTls",
"avgRequest",
"avgFirstByte",
"avgDownload",
] as const;
export const PHASE_COLOR_KEYS = {
avgDns: "dns",
avgTcp: "tcp",
avgTls: "tls",
avgRequest: "request",
avgFirstByte: "firstByte",
avgDownload: "download",
} as const;
// i18n keys, resolved with t() at render
export const PHASE_LABEL_KEYS: Record<(typeof PHASE_KEYS)[number], string> = {
avgDns: "common.charts.phases.dns",
avgTcp: "common.charts.phases.tcp",
avgTls: "common.charts.phases.tls",
avgRequest: "common.charts.phases.request",
avgFirstByte: "common.charts.phases.firstByte",
avgDownload: "common.charts.phases.download",
};
@@ -1,57 +1,43 @@
import Stack from "@mui/material/Stack";
import { BaseChart, BaseBox } from "@/Components/design-elements";
import { Clock } from "lucide-react";
import { Dot } from "@/Components/design-elements";
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
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 { XTick } from "@/Components/monitors/charts/XTick";
import { useSelector } from "react-redux";
import {
formatDateWithTz,
tickDateFormatLookup,
tooltipDateFormatLookup,
} from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import { formatDateWithTz, formatMs, tooltipDateFormatLookup } from "@/Utils/TimeUtils";
import { useTheme } from "@mui/material/styles";
import type { GroupedCheck } from "@/Types/Check";
import type { Theme } from "@mui/material/styles";
import type { GroupedCheck, GroupedUptimeCheck } from "@/Types/Check";
import type { RootState } from "@/Types/state";
import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils";
type XTickProps = {
x: number;
y: number;
payload: { value: any };
import { useMemo, useState } from "react";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import {
PHASE_COLOR_KEYS,
PHASE_KEYS,
PHASE_LABEL_KEYS,
} from "@/Components/monitors/charts/ChartUtils";
type DetailsCheck = GroupedCheck &
Partial<Pick<GroupedUptimeCheck, (typeof PHASE_KEYS)[number]>>;
type ResponseTimeToolTipProps = TooltipProps<ValueType, NameType> & {
range: string;
};
export const XTick = ({ x, y, payload, range }: XTickProps) => {
const format = tickDateFormatLookup(range);
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
type ResponseTimeToolTipProps = {
active?: boolean | undefined;
payload?: any[];
label?: string | number;
range: string;
theme: any;
theme: Theme;
uiTimezone: string;
};
@@ -63,16 +49,65 @@ const ResponseTimeToolTip = ({
theme,
uiTimezone,
}: ResponseTimeToolTipProps) => {
const { t } = useTranslation();
if (!label) return null;
if (!payload) return null;
if (!active) return null;
const format = tooltipDateFormatLookup(range);
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
const responseTime = payload[0]?.payload?.avgResponseTime ?? 0;
return (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
<Typography>Response time: {responseTime} ms</Typography>
<Typography>
{t("common.charts.histogram.responseTime", {
value: formatMs(responseTime),
})}
</Typography>
</BaseBox>
);
};
type TimingPhaseToolTipProps = TooltipProps<ValueType, NameType> & {
range: string;
theme: Theme;
uiTimezone: string;
};
const TimingPhasesToolTip = ({
active,
payload,
range,
theme,
uiTimezone,
}: TimingPhaseToolTipProps) => {
const { t } = useTranslation();
if (!active || !payload?.length) return null;
const bucket = payload[0].payload as GroupedUptimeCheck;
const total = PHASE_KEYS.reduce((sum, key) => sum + bucket[key], 0);
const format = tooltipDateFormatLookup(range);
return (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(bucket.bucketDate, format, uiTimezone)}</Typography>
<Typography>
{t("common.charts.histogram.total", { value: formatMs(total) })}
</Typography>
{PHASE_KEYS.map((key) => (
<Stack
alignItems={"center"}
direction="row"
key={key}
gap={SPACING.LG}
>
<Dot
size="lg"
color={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
/>
<Typography>
{t(PHASE_LABEL_KEYS[key])}: {formatMs(bucket[key])}
</Typography>
</Stack>
))}
</BaseBox>
);
};
@@ -81,11 +116,28 @@ export const HistogramDetails = ({
checks,
range,
}: {
checks: GroupedCheck[];
checks: DetailsCheck[];
range: string;
}) => {
const [showTimingPhases, setShowTimingPhases] = useState(false);
const [hoveredPhase, setHoveredPhase] = useState<string | null>(null);
const { t } = useTranslation();
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const yMax = useMemo(() => {
const totals = checks.map((check) => {
const phaseSum = PHASE_KEYS.reduce((sum, key) => sum + (check[key] ?? 0), 0);
return Math.max(check.avgResponseTime ?? 0, phaseSum);
});
return computeYAxisCap(totals);
}, [checks]);
const hasTimingPhases = useMemo(
() => (checks ?? []).some((check) => PHASE_KEYS.some((key) => (check[key] ?? 0) > 0)),
[checks]
);
return (
<BaseChart
icon={
@@ -94,7 +146,15 @@ export const HistogramDetails = ({
strokeWidth={1.5}
/>
}
title="Response times"
title={
showTimingPhases
? t("common.charts.labels.timingPhases")
: t("common.charts.labels.responseTime")
}
onClick={() => {
if (!hasTimingPhases) return;
setShowTimingPhases((prev) => !prev);
}}
>
<ResponsiveContainer
width="100%"
@@ -127,7 +187,34 @@ export const HistogramDetails = ({
stopOpacity={0}
/>
</linearGradient>
{PHASE_KEYS.map((key) => (
<linearGradient
key={key}
id={`phaseGradient-${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
stopOpacity={0.2}
/>
</linearGradient>
))}
</defs>
<YAxis
hide
// scale="sqrt"
domain={[0, yMax ?? "auto"]}
allowDataOverflow
/>
<XAxis
axisLine={false}
tickLine={false}
@@ -140,24 +227,99 @@ export const HistogramDetails = ({
)}
/>
<Tooltip
content={(props) => (
<ResponseTimeToolTip
{...props}
range={range}
theme={theme}
uiTimezone={uiTimezone}
/>
)}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.main}
fill="url(#colorUv)"
/>
{showTimingPhases ? (
<Tooltip
content={(props) => (
<TimingPhasesToolTip
{...props}
range={range}
theme={theme}
uiTimezone={uiTimezone}
/>
)}
/>
) : (
<Tooltip
content={(props) => (
<ResponseTimeToolTip
{...props}
range={range}
theme={theme}
uiTimezone={uiTimezone}
/>
)}
/>
)}
{!showTimingPhases && (
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.main}
fill="url(#colorUv)"
/>
)}
{hasTimingPhases &&
showTimingPhases &&
PHASE_KEYS.map((key) => {
const dimmed = hoveredPhase !== null && hoveredPhase !== key;
return (
<Area
key={key}
type="monotone"
stackId={1}
dataKey={key}
stroke={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
fill={`url(#phaseGradient-${key})`}
fillOpacity={dimmed ? 0.2 : undefined}
strokeOpacity={dimmed ? 0.2 : undefined}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
<Stack
justifyContent={"center"}
gap={LAYOUT.XS}
direction={"row"}
flexWrap={"wrap"}
>
{showTimingPhases ? (
PHASE_KEYS.map((key) => {
return (
<Stack
direction="row"
alignItems="center"
gap={SPACING.LG}
key={key}
onMouseEnter={() => setHoveredPhase(key)}
onMouseLeave={() => setHoveredPhase(null)}
>
<Dot
color={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
size="lg"
/>
<Typography lineHeight={1}>{t(PHASE_LABEL_KEYS[key])}</Typography>
</Stack>
);
})
) : (
<Stack
direction="row"
alignItems="center"
gap={SPACING.LG}
onMouseEnter={() => setHoveredPhase("avgResponseTime")}
onMouseLeave={() => setHoveredPhase(null)}
>
<Dot
color={theme.palette.primary.main}
size="lg"
/>
<Typography lineHeight={1}>
{t("common.charts.labels.totalResponseTime")}
</Typography>
</Stack>
)}
</Stack>
</BaseChart>
);
};
@@ -8,7 +8,7 @@ import {
ResponsiveContainer,
} from "recharts";
import { Fragment, useId } from "react";
import { XTick } from "@/Components/monitors";
import { XTick } from "@/Components/monitors/charts/XTick";
import { useTheme } from "@mui/material/styles";
import type { HardwareCheckStats } from "@/Types/Monitor";
@@ -1,6 +1,6 @@
import { BaseChart } from "@/Components/design-elements";
import { TrendingUp } from "lucide-react";
import { XTick } from "@/Components/monitors";
import { XTick } from "@/Components/monitors/charts/XTick";
import {
XAxis,
AreaChart,
@@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography";
import { BaseChart } from "@/Components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { formatDateWithTz, formatMs } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
@@ -100,7 +100,7 @@ export const HistogramStatus = ({
</Typography>
) : null}
<Typography variant="caption">{titleText}</Typography>
<Typography variant="body2">{Math.floor(avg)} ms</Typography>
<Typography variant="body2">{formatMs(avg)}</Typography>
</Stack>
);
};
@@ -7,6 +7,7 @@ import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
import { useTranslation } from "react-i18next";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import { formatMs } from "@/Utils/TimeUtils";
export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
const { t } = useTranslation();
@@ -20,7 +21,7 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
const msg: Record<string, string> = {
success: "Excellent",
warning: "Average",
danger: "Poor",
error: "Poor",
};
return (
@@ -84,7 +85,9 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
<Typography
variant="h1"
textAlign={"center"}
>{`${avg?.toFixed()}ms`}</Typography>
>
{formatMs(avg)}
</Typography>
</Stack>
</Stack>
</BaseChart>
@@ -0,0 +1,30 @@
import { formatDateWithTz, tickDateFormatLookup } from "@/Utils/TimeUtils";
import { useSelector } from "react-redux";
import { Text } from "recharts";
import { useTheme } from "@mui/material";
import type { RootState } from "@/Types/state";
type XTickProps = {
x: number;
y: number;
payload: { value: string };
range: string;
};
export const XTick = ({ x, y, payload, range }: XTickProps) => {
const format = tickDateFormatLookup(range);
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
+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";
+21 -9
View File
@@ -10,10 +10,11 @@ import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useNavigate } from "react-router";
import { MoreVertical, LogOut } from "lucide-react";
import { MoreVertical, LogOut, Moon, Sun } from "lucide-react";
import { Avatar, Icon } from "@/Components/design-elements";
import { clearAuthState } from "@/Features/Auth/authSlice.js";
import type { RootState } from "@/Types/state.js";
import { setMode } from "@/Features/UI/uiSlice.js";
interface AuthFooterProps {
collapsed: boolean;
@@ -27,6 +28,7 @@ interface AuthFooterProps {
export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => {
const { t } = useTranslation();
const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const user = useSelector((state: RootState) => state.auth.user);
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -105,13 +107,14 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
<Typography
fontWeight={500}
noWrap
sx={{ color: theme.palette.text.primary }}
color={theme.palette.text.primary}
>
{user?.firstName} {user?.lastName}
</Typography>
<Typography
noWrap
sx={{ textTransform: "capitalize", color: theme.palette.text.secondary }}
textTransform={"capitalize"}
color={theme.palette.text.secondary}
>
{getRoleText()}
</Typography>
@@ -154,15 +157,12 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
pb={2}
sx={{ pointerEvents: "none" }}
>
<Typography
fontWeight={500}
fontSize={13}
>
<Typography fontWeight={500}>
{user?.firstName} {user?.lastName}
</Typography>
<Typography
fontSize={12}
sx={{ textTransform: "capitalize" }}
variant="body2"
textTransform={"capitalize"}
>
{getRoleText()}
</Typography>
@@ -178,6 +178,18 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
{item.name}
</MenuItem>
))}
<MenuItem
onClick={() => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
handleMenuClose();
}}
sx={menuItemSx}
>
<Icon icon={mode === "light" ? Moon : Sun} />
{mode === "light"
? t("pages.settings.form.ui.option.theme.dark")
: t("pages.settings.form.ui.option.theme.light")}
</MenuItem>
<MenuItem
onClick={handleLogout}
sx={menuItemSx}
+6
View File
@@ -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",
+1
View File
@@ -102,6 +102,7 @@ export const Sidebar = () => {
})}
</List>
<StarPrompt />
<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;
+3 -1
View File
@@ -20,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,
};
};
+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]);
};
+134 -100
View File
@@ -1,5 +1,10 @@
import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import {
DefaultHttpMethod,
DefaultMonitorMatchMethod,
DefaultPageSpeedStrategy,
} from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions {
@@ -12,118 +17,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 || DefaultMonitorMatchMethod,
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: DefaultMonitorMatchMethod,
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 -72
View File
@@ -8,81 +8,17 @@ interface UseNotificationFormOptions {
}
function buildDefaults(data: Notification | null): NotificationFormData {
if (data?.type === "matrix") {
return {
type: "matrix",
notificationName: data.notificationName || "",
homeserverUrl: data.homeserverUrl || "",
roomId: data.roomId || "",
accessToken: data.accessToken || "",
};
}
if (data?.type === "telegram") {
return {
type: "telegram",
notificationName: data.notificationName || "",
address: data.address || "",
accessToken: data.accessToken || "",
};
}
if (data?.type === "slack") {
return {
type: "slack",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "discord") {
return {
type: "discord",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "webhook") {
return {
type: "webhook",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "pager_duty") {
return {
type: "pager_duty",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "teams") {
return {
type: "teams",
notificationName: data.notificationName || "",
address: data.address || "",
};
}
if (data?.type === "twilio") {
return {
type: "twilio",
notificationName: data.notificationName || "",
accountSid: data.accountSid || "",
accessToken: data.accessToken || "",
phone: data.phone || "",
twilioPhoneNumber: data.twilioPhoneNumber || "",
};
}
if (data?.type === "pushover") {
return {
type: "pushover",
notificationName: data.notificationName || "",
address: data.address || "",
accessToken: data.accessToken || "",
};
}
// Default: email (covers both data === null and data.type === "email")
return {
type: "email",
type: data?.type ?? "email",
notificationName: data?.notificationName || "",
address: data?.address || "",
accessToken: data?.accessToken || "",
accountSid: data?.accountSid || "",
phone: data?.phone || "",
twilioPhoneNumber: data?.twilioPhoneNumber || "",
homeserverUrl: data?.homeserverUrl || "",
roomId: data?.roomId || "",
topic: data?.topic || "",
};
}
+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,
+3 -1
View File
@@ -6,6 +6,7 @@ import {
DEFAULT_STATUS_PAGE_THEME_MODE,
} from "@/Types/StatusPage";
import type { Monitor } from "@/Types/Monitor";
import { resolveTimezone } from "@/Utils/TimeUtils";
interface UseStatusPageFormOptions {
data?: StatusPage | null;
@@ -30,7 +31,8 @@ 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 || "#13715B",
monitors: data?.monitors || [],
+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]);
};
+39 -103
View File
@@ -1,11 +1,6 @@
import { Trash2 } from "lucide-react";
import { TextField, Button, Autocomplete } from "@/Components/inputs";
import { TextField, Button } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import Divider from "@mui/material/Divider";
import FormHelperText from "@mui/material/FormHelperText";
import { useForm, Controller } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { ConfigBox, BasePage } from "@/Components/design-elements";
import { UserRoles } from "@/Types/User";
@@ -22,6 +17,8 @@ import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField";
interface RoleOption {
id: UserRole;
@@ -43,10 +40,11 @@ const EditUserPage = () => {
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
const { control, handleSubmit, reset, watch, setValue } = useForm<EditUserFormData>({
const form = useForm<EditUserFormData>({
resolver,
defaultValues: defaults,
});
const { handleSubmit, reset } = form;
useEffect(() => {
if (user) {
@@ -64,13 +62,6 @@ const EditUserPage = () => {
name: t(`common.auth.roles.${role}`),
}));
const watchedRoles = watch("role");
const selectedRoles = roleOptions.filter((r) => watchedRoles?.includes(r.id));
const handleRemoveRole = (roleToRemove: UserRole) => {
const newRoles = watchedRoles.filter((role) => role !== roleToRemove);
setValue("role", newRoles, { shouldValidate: true });
};
const canDeleteUser =
isAdmin &&
userId !== currentUser?.id &&
@@ -90,42 +81,31 @@ const EditUserPage = () => {
};
return (
<BasePage loading={isLoading}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...form}>
<BasePage
loading={isLoading}
component={"form"}
onSubmit={handleSubmit(onSubmit)}
>
<Stack gap={theme.spacing(8)}>
<ConfigBox
title={t("pages.account.form.name.title")}
subtitle={t("pages.account.form.name.description")}
rightContent={
<Stack gap={theme.spacing(8)}>
<Controller
<FormTextField
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}
autoComplete="given-name"
/>
)}
autoComplete="given-name"
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
/>
<Controller
<FormTextField
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}
autoComplete="family-name"
/>
)}
autoComplete="family-name"
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
/>
<TextField
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
@@ -139,55 +119,11 @@ const EditUserPage = () => {
title={t("pages.editUser.form.roles.title")}
subtitle={t("pages.editUser.form.roles.description")}
rightContent={
<Stack spacing={theme.spacing(4)}>
<Controller
name="role"
control={control}
render={({ field, fieldState }) => (
<>
<Autocomplete
fieldLabel={t("common.form.role.option.role.label")}
multiple
options={roleOptions}
value={selectedRoles}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: RoleOption[]) => {
field.onChange(newValue.map((r) => r.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
{fieldState.error && (
<FormHelperText error>{fieldState.error.message}</FormHelperText>
)}
</>
)}
/>
{selectedRoles.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedRoles.map((role, index) => (
<Stack
direction="row"
alignItems="center"
key={role.id}
width="100%"
>
<Typography flexGrow={1}>{role.name}</Typography>
<IconButton
size="small"
onClick={() => handleRemoveRole(role.id)}
aria-label="Remove role"
>
<Trash2 size={16} />
</IconButton>
{index < selectedRoles.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
<FormMultiSelectField
name="role"
fieldLabel={t("common.form.role.option.role.label")}
options={roleOptions}
/>
}
/>
<Stack
@@ -217,19 +153,19 @@ const EditUserPage = () => {
</Button>
</Stack>
</Stack>
</form>
<DialogInput
open={showDeleteDialog}
title={t("pages.editUser.dialog.removeUser.title")}
content={t("pages.editUser.dialog.removeUser.content", {
name: `${user?.firstName} ${user?.lastName}`,
})}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteUser}
confirmText={t("common.buttons.removeUser")}
loading={isDeleting}
/>
</BasePage>
<DialogInput
open={showDeleteDialog}
title={t("pages.editUser.dialog.removeUser.title")}
content={t("pages.editUser.dialog.removeUser.content", {
name: `${user?.firstName} ${user?.lastName}`,
})}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteUser}
confirmText={t("common.buttons.removeUser")}
loading={isDeleting}
/>
</BasePage>
</FormProvider>
);
};
+61 -82
View File
@@ -1,13 +1,15 @@
import { Stack } from "@mui/material";
import { ConfigBox } from "@/Components/design-elements";
import { TextField, Button } from "@/Components/inputs";
import { Button } from "@/Components/inputs";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
import { useForm, Controller } from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import { usePasswordForm } from "@/Hooks/usePasswordForm";
import { usePatch } from "@/Hooks/UseApi";
import type { PasswordFormData } from "@/Validation/password";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { LAYOUT } from "@/Utils/Theme/constants";
export const TabPassword = () => {
const { t } = useTranslation();
@@ -15,10 +17,11 @@ export const TabPassword = () => {
const { resolver, defaults } = usePasswordForm();
const { patch, loading } = usePatch<FormData, void>();
const { control, handleSubmit, reset } = useForm<PasswordFormData>({
const form = useForm<PasswordFormData>({
resolver,
defaultValues: defaults,
});
const { handleSubmit, reset } = form;
const onSubmit = async (data: PasswordFormData) => {
const fd = new FormData();
@@ -32,85 +35,61 @@ export const TabPassword = () => {
};
return (
<Stack
gap={theme.spacing(8)}
component="form"
onSubmit={handleSubmit(onSubmit)}
>
<ConfigBox
title={t("pages.account.form.currentPassword.title")}
subtitle={t("pages.account.form.currentPassword.description")}
rightContent={
<Controller
name="currentPassword"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="password"
fieldLabel={t("pages.account.form.currentPassword.option.label")}
placeholder={t("pages.account.form.currentPassword.option.placeholder")}
autoComplete="current-password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
}
/>
<ConfigBox
title={t("pages.account.form.newPassword.title")}
subtitle={t("pages.account.form.newPassword.description")}
rightContent={
<Stack gap={theme.spacing(8)}>
<Controller
name="newPassword"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="password"
fieldLabel={t(
"pages.account.form.newPassword.option.newPassword.label"
)}
placeholder={t(
"pages.account.form.newPassword.option.newPassword.placeholder"
)}
autoComplete="new-password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="confirm"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="password"
fieldLabel={t("pages.account.form.newPassword.option.confirm.label")}
placeholder={t(
"pages.account.form.newPassword.option.confirm.placeholder"
)}
autoComplete="new-password"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={loading}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
<FormProvider {...form}>
<Stack
gap={theme.spacing(LAYOUT.MD)}
component="form"
onSubmit={handleSubmit(onSubmit)}
>
{t("common.buttons.save")}
</Button>
</Stack>
<ConfigBox
title={t("pages.account.form.currentPassword.title")}
subtitle={t("pages.account.form.currentPassword.description")}
rightContent={
<FormTextField
name="currentPassword"
type="password"
autoComplete="current-password"
fieldLabel={t("pages.account.form.currentPassword.option.label")}
placeholder={t("pages.account.form.currentPassword.option.placeholder")}
/>
}
/>
<ConfigBox
title={t("pages.account.form.newPassword.title")}
subtitle={t("pages.account.form.newPassword.description")}
rightContent={
<Stack gap={theme.spacing(LAYOUT.MD)}>
<FormTextField
name="newPassword"
type="password"
autoComplete="new-password"
fieldLabel={t("pages.account.form.newPassword.option.newPassword.label")}
placeholder={t(
"pages.account.form.newPassword.option.newPassword.placeholder"
)}
/>
<FormTextField
name="confirm"
type="password"
autoComplete="new-password"
fieldLabel={t("pages.account.form.newPassword.option.confirm.label")}
placeholder={t(
"pages.account.form.newPassword.option.confirm.placeholder"
)}
/>
</Stack>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={loading}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
</FormProvider>
);
};
+52 -64
View File
@@ -1,11 +1,11 @@
import { Stack } from "@mui/material";
import { ConfigBox } from "@/Components/design-elements";
import { TextField, Button } from "@/Components/inputs";
import { Button } from "@/Components/inputs";
import { ImageUpload } from "@/Components/inputs";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useForm, Controller } from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import { useSelector, useDispatch } from "react-redux";
import { useProfileForm } from "@/Hooks/useProfileForm";
import { usePatch } from "@/Hooks/UseApi";
@@ -13,6 +13,8 @@ import { setUser } from "@/Features/Auth/authSlice";
import type { ProfileFormData } from "@/Validation/profile";
import type { RootState } from "@/Types/state";
import type { User } from "@/Types/User";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { LAYOUT } from "@/Utils/Theme/constants";
export const TabProfile = () => {
const theme = useTheme();
@@ -22,7 +24,7 @@ export const TabProfile = () => {
const { resolver, defaults } = useProfileForm();
const { patch, loading: patchLoading } = usePatch<FormData, User>();
const { control, handleSubmit, setValue, watch } = useForm<ProfileFormData>({
const form = useForm<ProfileFormData>({
resolver,
defaultValues: {
firstName: user?.firstName ?? defaults.firstName,
@@ -31,7 +33,7 @@ export const TabProfile = () => {
deleteProfileImage: defaults.deleteProfileImage,
},
});
const { handleSubmit, setValue, watch } = form;
const currentImage = watch("profileImage");
const deleteImage = watch("deleteProfileImage");
@@ -74,66 +76,52 @@ export const TabProfile = () => {
};
return (
<Stack
gap={theme.spacing(8)}
component="form"
onSubmit={handleSubmit(onSubmit)}
>
<ConfigBox
title={t("pages.account.form.name.title")}
subtitle={t("pages.account.form.name.description")}
rightContent={
<Stack gap={theme.spacing(8)}>
<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")}
autoComplete="given-name"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<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")}
autoComplete="family-name"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.account.form.photo.title")}
subtitle={t("pages.account.form.photo.description")}
rightContent={
<ImageUpload
src={getCurrentImageSrc()}
onChange={handleImageChange}
/>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={patchLoading}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
<FormProvider {...form}>
<Stack
gap={theme.spacing(LAYOUT.MD)}
component="form"
onSubmit={handleSubmit(onSubmit)}
>
{t("common.buttons.save")}
</Button>
</Stack>
<ConfigBox
title={t("pages.account.form.name.title")}
subtitle={t("pages.account.form.name.description")}
rightContent={
<Stack gap={theme.spacing(LAYOUT.MD)}>
<FormTextField
name="firstName"
autoComplete="given-name"
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
/>
<FormTextField
name="lastName"
autoComplete="family-name"
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.account.form.photo.title")}
subtitle={t("pages.account.form.photo.description")}
rightContent={
<ImageUpload
src={getCurrentImageSrc()}
onChange={handleImageChange}
/>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={patchLoading}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
</FormProvider>
);
};
@@ -1,15 +1,16 @@
import { Stack } from "@mui/material";
import MenuItem from "@mui/material/MenuItem";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useForm, Controller } from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
import { Dialog, TextField, Select } from "@/Components/inputs";
import { Dialog } from "@/Components/inputs";
import { useAddTeamMemberForm } from "@/Hooks/useAddTeamMemberForm";
import type { AddTeamMemberFormData } from "@/Validation/addTeamMember";
import type { UserRole, User } from "@/Types/User";
import type { User } from "@/Types/User";
import { usePost } from "@/Hooks/UseApi";
import { LAYOUT } from "@/Utils/Theme/constants";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { RoleSelectField } from "./RoleSelectField";
interface AddTeamMemberDialogProps {
open: boolean;
@@ -35,17 +36,13 @@ export const AddTeamMemberDialog = ({
const { schema, defaults } = useAddTeamMemberForm();
const { post, loading } = usePost<RegisterPayload, User>();
const { control, handleSubmit, reset, setError } = useForm<AddTeamMemberFormData>({
const form = useForm<AddTeamMemberFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
values: defaults,
});
const roleOptions: { value: UserRole; label: string }[] = [
{ value: "admin", label: t("common.auth.roles.admin") },
{ value: "user", label: t("common.auth.roles.user") },
{ value: "demo", label: t("common.auth.roles.demo") },
];
const { handleSubmit, reset, setError } = form;
const handleClose = () => {
reset();
@@ -55,7 +52,7 @@ export const AddTeamMemberDialog = ({
const onSubmit = async (data: AddTeamMemberFormData) => {
if (loading) return;
const { confirm, ...userData } = data;
const { confirm: _confirm, ...userData } = data;
const payload: RegisterPayload = userData;
const result = await post("/auth/users", payload);
@@ -72,124 +69,64 @@ export const AddTeamMemberDialog = ({
};
return (
<Dialog
open={open}
title={t("pages.account.team.addMember.title")}
content={t("pages.account.team.addMember.description")}
onCancel={handleClose}
onConfirm={handleSubmit(onSubmit)}
confirmText={t("common.buttons.addMember")}
loading={loading}
maxWidth="sm"
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}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
type="email"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
<FormProvider {...form}>
<Dialog
open={open}
title={t("pages.account.team.addMember.title")}
content={t("pages.account.team.addMember.description")}
onCancel={handleClose}
onConfirm={handleSubmit(onSubmit)}
confirmText={t("common.buttons.addMember")}
loading={loading}
maxWidth="sm"
fullWidth
>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
>
<FormTextField
name="firstName"
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
/>
)}
/>
<Controller
name="role"
control={control}
render={({ field }) => (
<Select
{...field}
value={field.value[0] ?? "user"}
onChange={(e) => field.onChange([e.target.value])}
fieldLabel={t("common.form.role.option.role.label")}
fullWidth
>
{roleOptions.map((option) => (
<MenuItem
key={option.value}
value={option.value}
>
{option.label}
</MenuItem>
))}
</Select>
)}
/>
<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 }}
/>
)}
<FormTextField
name="lastName"
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
/>
</Stack>
<FormTextField
name="email"
type="email"
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
/>
<RoleSelectField />
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
>
<FormTextField
name="password"
fieldLabel={t("pages.auth.common.form.option.password.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
sx={{ flex: 1 }}
/>
<FormTextField
name="confirm"
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
placeholder={t("pages.auth.common.form.option.password.placeholder")}
type="password"
/>
</Stack>
</Stack>
</Stack>
</Dialog>
</Dialog>
</FormProvider>
);
};
Loaded 100 of 701 files, more files were not shown because too many files have changed in this diff. Show more