Compare commits

..
Author SHA1 Message Date
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 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
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
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
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
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
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
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
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
423 changed files with 21507 additions and 6897 deletions

No files matched your search

Regular → Executable
+21 -2
View File
@@ -1,4 +1,23 @@
#!/bin/sh
# Skip during merge/rebase/cherry-pick/revert. lint-staged's internal
# git-stash mechanism is incompatible with these states and will silently
# wipe MERGE_HEAD and your conflict resolutions on commit failure.
git_dir=$(git rev-parse --git-dir)
if [ -e "$git_dir/MERGE_HEAD" ] \
|| [ -e "$git_dir/CHERRY_PICK_HEAD" ] \
|| [ -e "$git_dir/REVERT_HEAD" ] \
|| [ -d "$git_dir/rebase-merge" ] \
|| [ -d "$git_dir/rebase-apply" ]; then
echo "Skipping lint-staged (mid merge/rebase/cherry-pick/revert)"
exit 0
fi
# Anchor to repo root so commits made from a subdirectory still work, and
# run each package's lint-staged in a subshell so cwd is scoped (a failure
# inside one package can't poison the cwd for the next).
cd "$(git rev-parse --show-toplevel)" || exit 1
echo "Running lint-staged..."
cd client && npx lint-staged && cd ..
cd server && npx lint-staged && cd ..
(cd client && npx lint-staged) || exit 1
(cd server && npx lint-staged) || exit 1
+16 -12
View File
@@ -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
+1
View File
@@ -151,6 +151,7 @@ LOG_LEVEL="debug"
- `ORIGIN`: Origin for CORS purposes
- `TOKEN_TTL`: Token time to live (in vercel/ms format)
- `LOG_LEVEL`: Debug level (debug, info, warn, error)
- `QUEUE_TYPE`: Job queue implementation (`superSimpleQueue` or `lessSimpleQueue`, optional, default: `superSimpleQueue`)
Start the backend server:
-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.
+31 -1
View File
@@ -1,5 +1,25 @@
<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.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)
@@ -40,12 +60,14 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
- [🤝 Contributing](#contributing)
<a id="demo"></a>
## Demo
You can see the latest build of [Checkmate](https://demo.checkmate.so/) in action.
The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
<a id="users-guide"></a>
## User's guide
Usage instructions can be found [here](https://checkmate.so/docs).
@@ -54,6 +76,7 @@ Usage instructions can be found [here](https://checkmate.so/docs).
- [Docker](https://www.docker.com/) installed
- [Git](https://git-scm.com/) installed
<a id="installation"></a>
## Installation
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
@@ -66,6 +89,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:
@@ -76,6 +100,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:
@@ -85,6 +110,7 @@ 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)
@@ -109,6 +135,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
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>
@@ -126,6 +153,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
<a id="tech-stack"></a>
## Tech stack
- [ReactJs](https://react.dev/)
@@ -135,6 +163,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
- [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".
@@ -142,6 +171,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
- 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.
+236 -4
View File
@@ -1,12 +1,12 @@
{
"name": "client",
"version": "3.7.1",
"version": "3.8.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client",
"version": "3.7.1",
"version": "3.8.1",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
@@ -57,6 +57,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"lint-staged": "^16.2.7",
"mongoose": "9.6.2",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "^6.4.2"
@@ -84,6 +85,7 @@
"node_modules/@babel/core": {
"version": "7.29.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -354,6 +356,7 @@
"node_modules/@emotion/react": {
"version": "11.14.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -391,6 +394,7 @@
"node_modules/@emotion/styled": {
"version": "11.14.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1107,6 +1111,16 @@
"supercluster": "^8.0.1"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
"integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.7",
"license": "MIT",
@@ -1160,6 +1174,7 @@
"node_modules/@mui/material": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.7",
@@ -1264,6 +1279,7 @@
"node_modules/@mui/system": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.7",
@@ -2304,6 +2320,7 @@
"version": "24.5.2",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.12.0"
}
@@ -2319,6 +2336,7 @@
"node_modules/@types/react": {
"version": "18.3.27",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -2352,6 +2370,23 @@
"version": "0.0.6",
"license": "MIT"
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -2392,6 +2427,7 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@@ -2672,6 +2708,7 @@
"version": "8.15.0",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3006,6 +3043,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3020,6 +3058,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bson": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/bytewise": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
@@ -3437,7 +3485,8 @@
},
"node_modules/dayjs": {
"version": "1.11.13",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -3832,6 +3881,7 @@
"version": "8.57.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4627,6 +4677,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -5222,6 +5273,16 @@
"node": ">=4.0"
}
},
"node_modules/kareem": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz",
"integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
@@ -5412,6 +5473,7 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5451,6 +5513,13 @@
"node": ">= 0.4"
}
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"dev": true,
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5540,6 +5609,109 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mongodb": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
"integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^7.2.0",
"mongodb-connection-string-url": "^7.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.806.0",
"@mongodb-js/zstd": "^7.0.0",
"gcp-metadata": "^7.0.1",
"kerberos": "^7.0.0",
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
"snappy": "^7.3.2",
"socks": "^2.8.6"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^13.0.0",
"whatwg-url": "^14.1.0"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/mongoose": {
"version": "9.6.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.2.tgz",
"integrity": "sha512-7m8HntjkoRnwEmuPC0kdlwcZXJOQf4twumFj+PNzg/anqqZE2Er7hQslqyzy07mP3JcFjoTSgH5765PyqOXsxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"kareem": "3.3.0",
"mongodb": "~7.2",
"mpath": "0.9.0",
"mquery": "6.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mpath": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
"integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"license": "MIT"
@@ -6078,6 +6250,7 @@
"node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -6089,6 +6262,7 @@
"node_modules/react-hook-form": {
"version": "7.71.1",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -6288,7 +6462,8 @@
},
"node_modules/redux": {
"version": "5.0.1",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-persist": {
"version": "6.0.0",
@@ -6438,6 +6613,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -6713,6 +6889,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sift": {
"version": "17.1.3",
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
"dev": true,
"license": "MIT"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -6823,6 +7006,16 @@
"node": ">=0.10.0"
}
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -7136,6 +7329,19 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -7249,6 +7455,7 @@
"version": "5.9.3",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7384,6 +7591,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -7472,6 +7680,30 @@
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": {
"version": "2.0.2",
"dev": true,
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "3.7.1",
"version": "3.8.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -14,7 +14,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "prettier --write"
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@emotion/react": "^11.13.3",
@@ -66,6 +66,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"lint-staged": "^16.2.7",
"mongoose": "9.6.2",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "^6.4.2"
@@ -35,7 +35,7 @@ const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats
}
const responseTimes = validChecks.map((check) => check.originalResponseTime);
const max = Math.max(...responseTimes);
const max = Math.round(Math.max(...responseTimes));
const avg = Math.round(
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
);
@@ -73,14 +73,20 @@ export const HistogramResponseTime = ({
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
minWidth={70}
width={110}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
>
<Typography variant="body2">
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography variant="body2">
<Typography
variant="body2"
noWrap
>
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
+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,
@@ -1,12 +1,13 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/design-elements";
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -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,6 +42,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
>
{formatUrl(monitor?.url)}
</Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && (
<>
<Dot />
@@ -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>
);
};
@@ -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";
+25 -19
View File
@@ -14,17 +14,19 @@ type AutoCompleteInputProps = Omit<
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
fieldLabel?: string;
required?: boolean;
renderOptionContent?: (option: any) => React.ReactNode;
};
export const AutoCompleteInput = ({
fieldLabel,
required,
renderInput,
renderOptionContent,
...props
}: AutoCompleteInputProps) => {
const theme = useTheme();
const multiple = props.multiple;
const { renderOption: customRenderOption, ...autocompleteProps } = props;
const defaultRenderInput = (params: any) => (
<TextField
{...params}
@@ -34,7 +36,7 @@ export const AutoCompleteInput = ({
const autocomplete = (
<Autocomplete
{...props}
{...autocompleteProps}
disableCloseOnSelect={!!multiple}
popupIcon={
<ChevronDown
@@ -46,24 +48,28 @@ export const AutoCompleteInput = ({
renderInput={renderInput || defaultRenderInput}
getOptionKey={(option) => option.id}
renderTags={() => null}
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
renderOption={
customRenderOption ||
((props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
{multiple && <Checkbox checked={selected} />}
{option.name}
</Stack>
</ListItem>
);
}}
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
width="100%"
>
{multiple && <Checkbox checked={selected} />}
{renderOptionContent ? renderOptionContent(option) : option.name}
</Stack>
</ListItem>
);
})
}
sx={{
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
+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,6 +10,7 @@ import type { RootState } from "@/Types/state";
export const languageNames: Record<string, string> = {
en: "English",
de: "Deutsch",
ca: "Català",
es: "Español",
fr: "Français",
"pt-BR": "Português (BR)",
@@ -1,9 +1,11 @@
import Stack from "@mui/material/Stack";
import { Select, Button } from "@/Components/inputs";
import { Select, Autocomplete, Button, TextField } from "@/Components/inputs";
import { ColoredLabel } from "@/Components/design-elements";
import MenuItem from "@mui/material/MenuItem";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { MonitorType } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import { Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
@@ -28,22 +30,34 @@ export const ControlsFilter = ({
setSelectedStatus,
selectedState,
setSelectedState,
tagOptions,
selectedTags,
setSelectedTags,
onClearFilters,
}: {
showTypes?: boolean;
selectedTypes?: MonitorType[];
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
selectedStatus: string;
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
selectedState: string;
setSelectedState: React.Dispatch<React.SetStateAction<string>>;
selectedStatus?: string;
setSelectedStatus?: React.Dispatch<React.SetStateAction<string>>;
selectedState?: string;
setSelectedState?: React.Dispatch<React.SetStateAction<string>>;
tagOptions?: Tag[];
selectedTags?: string[];
setSelectedTags?: React.Dispatch<React.SetStateAction<string[]>>;
onClearFilters: () => void;
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isFilterActive =
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
(selectedTypes?.length ?? 0) > 0 ||
Boolean(selectedStatus) ||
Boolean(selectedState) ||
(selectedTags?.length ?? 0) > 0;
const selectedTagObjects = (tagOptions ?? []).filter((tag) =>
(selectedTags ?? []).includes(tag.id)
);
return (
<Stack
direction={isSmall ? "column" : "row"}
@@ -66,34 +80,68 @@ export const ControlsFilter = ({
))}
</Select>
)}
<Select
placeholder="Status"
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
>
{statuses.map((status) => (
<MenuItem
key={status}
value={status}
>
<Typography textTransform={"capitalize"}>{status}</Typography>
</MenuItem>
))}
</Select>
<Select
placeholder="State"
value={selectedState}
onChange={(e) => setSelectedState(e.target.value)}
>
{states.map((state) => (
<MenuItem
key={state}
value={state}
>
<Typography textTransform={"capitalize"}>{state}</Typography>
</MenuItem>
))}
</Select>
{setSelectedStatus && (
<Select
placeholder="Status"
value={selectedStatus ?? ""}
onChange={(e) => setSelectedStatus(e.target.value)}
>
{statuses.map((status) => (
<MenuItem
key={status}
value={status}
>
<Typography textTransform={"capitalize"}>{status}</Typography>
</MenuItem>
))}
</Select>
)}
{setSelectedState && (
<Select
placeholder="State"
value={selectedState ?? ""}
onChange={(e) => setSelectedState(e.target.value)}
>
{states.map((state) => (
<MenuItem
key={state}
value={state}
>
<Typography textTransform={"capitalize"}>{state}</Typography>
</MenuItem>
))}
</Select>
)}
{tagOptions && setSelectedTags && (
<Autocomplete
multiple
options={tagOptions}
value={selectedTagObjects}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: Tag[]) => {
setSelectedTags(newValue.map((tag) => tag.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderOptionContent={(option) => (
<ColoredLabel
text={option.name}
color={option.color}
/>
)}
renderInput={(params) => (
<TextField
{...params}
placeholder={t("common.table.headers.tags")}
/>
)}
slotProps={{
popper: {
sx: { width: "fit-content !important", minWidth: 280 },
},
}}
sx={{ minWidth: 180 }}
/>
)}
{isFilterActive && (
<Button
variant="contained"
@@ -3,6 +3,8 @@ import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
import Map, { Marker, Popup } from "react-map-gl/maplibre";
import type { MapRef } from "react-map-gl/maplibre";
import type { FlatGeoCheck } from "@/Types/GeoCheck";
import { useTranslation } from "react-i18next";
import { formatStatusCode } from "@/Utils/statusCode";
import "maplibre-gl/dist/maplibre-gl.css";
interface GeoChecksMapProps {
@@ -14,6 +16,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
const { t } = useTranslation();
const mapPopupStyles = (
<GlobalStyles
@@ -174,7 +177,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
>
Status Code:
</Typography>{" "}
{selectedCheck.statusCode}
{formatStatusCode(selectedCheck.statusCode, t)}
</Typography>
<Typography variant="body2">
<Typography
@@ -9,6 +9,8 @@ import {
ResponsiveContainer,
Text,
} from "recharts";
import type { TooltipProps } from "recharts";
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
import Typography from "@mui/material/Typography";
import { useSelector } from "react-redux";
@@ -18,13 +20,14 @@ import {
tooltipDateFormatLookup,
} from "@/Utils/TimeUtils";
import { useTheme } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
import type { GroupedCheck } from "@/Types/Check";
import type { RootState } from "@/Types/state";
type XTickProps = {
x: number;
y: number;
payload: { value: any };
payload: { value: string };
range: string;
};
@@ -46,12 +49,9 @@ export const XTick = ({ x, y, payload, range }: XTickProps) => {
);
};
type ResponseTimeToolTipProps = {
active?: boolean | undefined;
payload?: any[];
label?: string | number;
type ResponseTimeToolTipProps = TooltipProps<ValueType, NameType> & {
range: string;
theme: any;
theme: Theme;
uiTimezone: string;
};
@@ -68,7 +68,8 @@ const ResponseTimeToolTip = ({
if (!active) return null;
const format = tooltipDateFormatLookup(range);
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
console.log(payload);
const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0);
return (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
+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",
+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 };
};
+14 -3
View File
@@ -9,7 +9,10 @@ import type { Monitor } from "@/Types/Monitor";
interface ApiResponse {
success: boolean;
msg: string;
data: Monitor[];
data: {
monitors: Monitor[];
failedCount: number;
};
}
interface UseBulkMonitorActionsReturn {
@@ -25,7 +28,7 @@ export const useBulkMonitorActions = (
page?: number
): UseBulkMonitorActionsReturn => {
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const { toastSuccess, toastError, toastInfo } = useToast();
const { toastSuccess, toastError, toastInfo, toastWarning } = useToast();
const { t } = useTranslation();
// Clear selection when page changes
@@ -40,13 +43,21 @@ export const useBulkMonitorActions = (
pause,
});
const affectedCount = res.data?.data?.length ?? 0;
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"
+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]);
};
+126 -100
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions {
@@ -12,118 +13,143 @@ 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 || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
useAdvancedMatching: data?.useAdvancedMatching || false,
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
};
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: "",
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: "",
expectedValue: "",
jsonPath: "",
};
}
return defaults;
};
export const useMonitorForm = ({
data = null,
defaultType = "http",
}: UseMonitorFormOptions = {}) => {
return useMemo(() => {
const type = data?.type || defaultType;
const base = getBaseDefaults(data);
let defaults: MonitorFormData;
switch (type) {
case "http":
defaults = {
...base,
type: "http",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
useAdvancedMatching: data?.useAdvancedMatching || false,
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
};
break;
case "ping":
defaults = {
...base,
type: "ping",
url: data?.url || "",
};
break;
case "port":
defaults = {
...base,
type: "port",
url: data?.url || "",
port: data?.port || 80,
};
break;
case "docker":
defaults = {
...base,
type: "docker",
url: data?.url || "",
};
break;
case "game":
defaults = {
...base,
type: "game",
url: data?.url || "",
port: data?.port || 27015,
gameId: data?.gameId || "",
};
break;
case "grpc":
defaults = {
...base,
type: "grpc",
url: data?.url || "",
port: data?.port || 50051,
grpcServiceName: data?.grpcServiceName || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
case "pagespeed":
defaults = {
...base,
type: "pagespeed",
url: data?.url || "",
};
break;
case "hardware":
defaults = {
...base,
type: "hardware",
url: data?.url || "",
secret: data?.secret || "",
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
selectedDisks: data?.selectedDisks || [],
};
break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default:
defaults = {
...base,
type: "http",
url: "",
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: "",
expectedValue: "",
jsonPath: "",
};
}
return { schema: monitorSchema, defaults };
return { schema: monitorSchema, defaults: getMonitorDefaults(type, data) };
}, [data, defaultType]);
};
+8
View File
@@ -78,6 +78,14 @@ function buildDefaults(data: Notification | null): NotificationFormData {
accessToken: data.accessToken || "",
};
}
if (data?.type === "ntfy") {
return {
type: "ntfy",
notificationName: data.notificationName || "",
address: data.address || "",
topic: data.topic || "",
};
}
// Default: email (covers both data === null and data.type === "email")
return {
type: "email",
+1
View File
@@ -31,6 +31,7 @@ export const useStatusPageForm = ({
const defaults: StatusPageFormData = {
companyName: data?.companyName || "",
url: data?.url || generateDefaultUrl(),
customDomain: data?.customDomain ?? null,
timezone: resolveTimezone(data?.timezone),
type: data?.type || ["uptime"],
color: data?.color || "#13715B",
+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]);
};
@@ -1,8 +1,12 @@
import { Table, Pagination, ValueLabel, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StatusCodeLabel,
} from "@/Components/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/design-elements/Table";
import type { Monitor } from "@/Types/Monitor";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { Check } from "@/Types/Check";
@@ -29,7 +33,7 @@ export const ChecksTable = ({
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const getHeaders = (t: Function, uiTimezone: string) => {
const getHeaders = () => {
const headers: Header<Check>[] = [
{
id: "monitorName",
@@ -63,13 +67,10 @@ export const ChecksTable = ({
id: "statusCode",
content: t("pages.checks.table.headers.statusCode"),
render: (row) => {
const code = row.statusCode;
if (!code) return "N/A";
const value = code < 300 ? "positive" : code < 400 ? "neutral" : "negative";
return (
<ValueLabel
value={value}
text={String(code)}
<StatusCodeLabel
statusCode={row.statusCode}
message={row.message}
/>
);
},
@@ -85,7 +86,7 @@ export const ChecksTable = ({
return headers;
};
const headers = getHeaders(t, uiTimezone);
const headers = getHeaders();
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
File diff suppressed because it is too large. Load diff
@@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack";
import Grid from "@mui/material/Grid";
import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography";
import { BaseBox, ValueLabel } from "@/Components/design-elements";
import { BaseBox, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useTranslation } from "react-i18next";
@@ -46,6 +46,7 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
if (!incident) {
return null;
}
return (
<Stack
gap={theme.spacing(LAYOUT.MD)}
@@ -123,7 +124,12 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
<Cell>{t("pages.incidents.dialog.details.statusCode")}</Cell>
</Grid>
<Grid size={4}>
<Cell>{incident.statusCode ?? "N/A"}</Cell>
<Cell>
<StatusCodeLabel
statusCode={incident.statusCode}
message={incident.message}
/>
</Cell>
</Grid>
<Grid size={2}>
<Cell>{t("pages.incidents.dialog.details.downtime")}</Cell>
@@ -1,8 +1,6 @@
import { Table } from "@/Components/design-elements";
import { Table, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
import { Pagination } from "@/Components/design-elements/Table";
import type { Header } from "@/Components/design-elements/Table";
import { ValueLabel } from "@/Components/design-elements";
import type { ValueType } from "@/Components/design-elements/StatusLabel";
import { ActionsMenu } from "@/Components/actions-menu";
import type { Incident } from "@/Types/Incident";
import type { Monitor } from "@/Types/Monitor";
@@ -136,13 +134,12 @@ export const IncidentsTable = ({
return (
<Typography
variant="body2"
sx={{
textTransform: "capitalize",
color:
row.resolutionType === "manual"
? theme.palette.warning.main
: theme.palette.success.main,
}}
color={
row.resolutionType === "manual"
? theme.palette.warning.main
: theme.palette.success.main
}
textTransform={"capitalize"}
>
{row.resolutionType}
</Typography>
@@ -155,15 +152,10 @@ export const IncidentsTable = ({
id: "statusCode",
content: t("pages.incidents.table.headers.statusCode"),
render: (row) => {
const code = row.statusCode;
if (!code) return "N/A";
let value: ValueType = "neutral";
if (code < 300) value = "positive";
else if (code >= 400) value = "negative";
return (
<ValueLabel
value={value}
text={String(code)}
<StatusCodeLabel
statusCode={row.statusCode}
message={row.message}
/>
);
},
@@ -1,7 +1,14 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Table, Pagination, StatusLabel, Gauge } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
Gauge,
ColoredLabel,
} from "@/Components/design-elements";
import { SPACING } from "@/Utils/Theme/constants";
import type { Header } from "@/Components/design-elements/Table";
import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu";
import { ArrowUp, ArrowDown } from "lucide-react";
@@ -13,10 +20,12 @@ import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import { Checkbox } from "@/Components/inputs";
interface InfraMonitorsTableProps {
monitors: Monitor[];
tags: Tag[];
refetch: () => void;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
@@ -34,6 +43,7 @@ interface InfraMonitorsTableProps {
export const InfraMonitorsTable = ({
monitors,
tags,
refetch,
setSelectedMonitor,
sortField,
@@ -267,6 +277,32 @@ export const InfraMonitorsTable = ({
return <Gauge progress={diskUsage} />;
},
},
{
id: "tags",
content: t("common.table.headers.tags"),
render: (row) => {
if (row.tags.length === 0) return "-";
return (
<Stack
justifyContent={"center"}
direction={isSmall ? "column" : "row"}
gap={theme.spacing(SPACING.LG)}
>
{row.tags.map((tagId) => {
const fullTag = tags.find((t) => t.id === tagId);
if (!fullTag) return null;
return (
<ColoredLabel
key={fullTag.id}
text={fullTag.name}
color={fullTag.color}
/>
);
})}
</Stack>
);
},
},
{
id: "actions",
@@ -1,6 +1,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { MonitorBasePageWithStates } from "@/Components/design-elements";
import { MonitorBasePageWithStates, ColoredLabel } from "@/Components/design-elements";
import { HeaderCreate } from "@/Components/common";
import {
ControlsFilter,
@@ -13,7 +14,9 @@ import { Play, Pause } from "lucide-react";
import { useGet, useDelete } from "@/Hooks/UseApi";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import type { Monitor, MonitorsWithChecksResponse } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import { useTheme } from "@mui/material";
import { SPACING } from "@/Utils/Theme/constants";
import { useState, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
@@ -38,12 +41,15 @@ const InfrastructureMonitors = () => {
// Local state
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [selectedState, setSelectedState] = useState<string>("");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [search, setSearch] = useState<string>("");
const [page, setPage] = useState<number>(0);
const [sortField, setSortField] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const { data: tags } = useGet<Tag[]>("/tags/team");
const isDialogOpen = Boolean(selectedMonitor);
const debouncedSearch = useDebounce<string>(search, 300);
@@ -51,6 +57,7 @@ const InfrastructureMonitors = () => {
const handleClearFilters = useCallback(() => {
setSelectedStatus("");
setSelectedState("");
setSelectedTags([]);
setSearch("");
}, []);
@@ -81,6 +88,7 @@ const InfrastructureMonitors = () => {
const monitorsWithChecksUrl = useMemo(() => {
const params = new URLSearchParams();
params.append("type", "hardware");
selectedTags.forEach((tagId) => params.append("tags", tagId));
params.append("limit", "1");
if (page !== undefined) params.append("page", String(page));
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
@@ -88,7 +96,7 @@ const InfrastructureMonitors = () => {
if (field) params.append("field", field);
if (sortOrder) params.append("order", sortOrder);
return `/monitors/team/with-checks?${params.toString()}`;
}, [page, rowsPerPage, filter, field, sortOrder]);
}, [page, rowsPerPage, filter, field, sortOrder, selectedTags]);
const {
data: monitorsWithChecksData,
@@ -114,7 +122,9 @@ const InfrastructureMonitors = () => {
} = useBulkMonitorActions(refetch, page);
// Check if any filters are active
const hasActiveFilters = Boolean(selectedStatus || selectedState || search);
const hasActiveFilters = Boolean(
selectedStatus || selectedState || selectedTags.length > 0 || search
);
// Show empty state only when there are truly no monitors (not just filtered out)
const effectiveTotalCount =
@@ -165,6 +175,9 @@ const InfrastructureMonitors = () => {
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
tagOptions={tags ?? []}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
onClearFilters={handleClearFilters}
/>
<TextField
@@ -176,6 +189,30 @@ const InfrastructureMonitors = () => {
/>
</Stack>
{selectedTags.length > 0 && (
<Stack
direction={isSmall ? "column" : "row"}
alignItems={isSmall ? "flex-start" : "center"}
flexWrap="wrap"
gap={theme.spacing(SPACING.XL)}
>
<Typography color={theme.palette.text.secondary}>
{t("pages.uptime.filters.activeTags")}
</Typography>
{selectedTags.map((tagId) => {
const tag = tags?.find((t) => t.id === tagId);
if (!tag) return null;
return (
<ColoredLabel
key={tag.id}
text={tag.name}
color={tag.color}
/>
);
})}
</Stack>
)}
{!isLoading && (
<BulkActionsBar
selectedCount={selectedRows.length}
@@ -200,6 +237,7 @@ const InfrastructureMonitors = () => {
<InfraMonitorsTable
monitors={monitorsWithChecksData?.monitors || []}
tags={tags ?? []}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
sortField={sortField}
+34 -7
View File
@@ -1,12 +1,15 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Stats } from "@/Pages/Logs/components/Stats";
import { MongoStats } from "@/Pages/Logs/components/MongoStats";
import { MongoCollectionStats } from "./components/MongoCollectionStats";
import { StatGauges } from "@/Pages/Logs/components/StatGauges";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useGet } from "@/Hooks/UseApi";
import type { Diagnostics } from "@/Types/Diagnostics";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
export const TabDiagnostics = () => {
const theme = useTheme();
@@ -19,12 +22,12 @@ export const TabDiagnostics = () => {
} = useGet<Diagnostics>("/diagnostic/system", {}, { refreshInterval: 5000 });
return (
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(4)}>
<Stack gap={theme.spacing(1)}>
<Stack gap={theme.spacing(LAYOUT.XL)}>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Stack gap={theme.spacing(SPACING.SM)}>
<Typography
variant="eyebrow"
color="text.secondary"
color={theme.palette.text.secondary}
>
{t("pages.logs.diagnostics.sections.runtimeStats.title")}
</Typography>
@@ -34,11 +37,11 @@ export const TabDiagnostics = () => {
</Stack>
<Stats diagnostics={diagnostics} />
</Stack>
<Stack gap={theme.spacing(4)}>
<Stack gap={theme.spacing(1)}>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Stack gap={theme.spacing(SPACING.SM)}>
<Typography
variant="eyebrow"
color="text.secondary"
color={theme.palette.text.secondary}
>
{t("pages.logs.diagnostics.sections.memoryCpu.title")}
</Typography>
@@ -48,6 +51,30 @@ export const TabDiagnostics = () => {
</Stack>
<StatGauges diagnostics={diagnostics} />
</Stack>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Typography
variant="eyebrow"
color={theme.palette.text.secondary}
>
{t("pages.logs.diagnostics.sections.mongoDBStats.title")}
</Typography>
<Typography color={theme.palette.text.secondary}>
{t("pages.logs.diagnostics.sections.mongoDBStats.description")}
</Typography>
<MongoStats diagnostics={diagnostics} />
</Stack>
<Stack gap={theme.spacing(LAYOUT.XS)}>
<Typography
variant="eyebrow"
color={theme.palette.text.secondary}
>
{t("pages.logs.diagnostics.sections.mongoDBCollectionStats.title")}
</Typography>
<Typography color={theme.palette.text.secondary}>
{t("pages.logs.diagnostics.sections.mongoDBCollectionStats.description")}
</Typography>
<MongoCollectionStats diagnostics={diagnostics} />
</Stack>
</Stack>
);
};
+53 -3
View File
@@ -2,8 +2,9 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { TableJobs, TableFailedJobs } from "./components/TableJobs";
import { TableWorkers } from "./components/TableWorkers";
import { useTheme } from "@mui/material";
import { useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import { useGet, usePost } from "@/Hooks/UseApi";
import type { QueueData } from "@/Types/Queue";
@@ -15,16 +16,27 @@ import { SPACING } from "@/Utils/Theme/constants";
export const TabQueue = () => {
const theme = useTheme();
const { t } = useTranslation();
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const {
data: queueData,
isLoading,
// error,
refetch,
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 1000 });
} = useGet<QueueData>(
`/queue/all-metrics?page=${page}&rowsPerPage=${rowsPerPage}`,
{},
{ refreshInterval: 1000, keepPreviousData: true }
);
const { post, loading: isFlushing } = usePost();
const jobs = queueData?.jobs ?? [];
const jobsCount = queueData?.count ?? 0;
const workers = (queueData?.metrics?.workers ?? []).map((worker, idx) => ({
...worker,
id: idx,
}));
const metrics = queueData?.metrics ?? null;
const hasNeverRun = !isLoading && metrics !== null && (metrics.totalRuns ?? 0) === 0;
const noQueueData = !isLoading && metrics === null && jobs.length === 0;
@@ -46,6 +58,34 @@ export const TabQueue = () => {
return (
<Stack gap={theme.spacing(16)}>
<Metrics metrics={metrics} />
<Stack gap={theme.spacing(3)}>
<Stack gap={theme.spacing(1)}>
<Typography
variant="eyebrow"
color={theme.palette.text.secondary}
>
{t("pages.logs.workers")}
</Typography>
<Typography color={theme.palette.text.secondary}>
<Trans
i18nKey="pages.logs.workersExplainers"
components={{
highlight: (
<Box
component="span"
bgcolor={theme.palette.rowStatus.running}
color={theme.palette.text.primary}
px={SPACING.SM}
py={SPACING.XXS}
borderRadius={theme.shape.borderRadius}
/>
),
}}
/>
</Typography>
</Stack>
<TableWorkers queueWorkers={workers} />
</Stack>
<Stack gap={theme.spacing(3)}>
<Stack gap={theme.spacing(1)}>
<Typography
@@ -72,7 +112,17 @@ export const TabQueue = () => {
/>
</Typography>
</Stack>
<TableJobs jobs={jobs} />
<TableJobs
jobs={jobs}
count={jobsCount}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={(_, newPage) => setPage(newPage)}
onRowsPerPageChange={(event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
}}
/>
</Stack>
<Stack gap={theme.spacing(3)}>
<Stack gap={theme.spacing(1)}>
@@ -0,0 +1,104 @@
import Box from "@mui/material/Box";
import { StatBox, Gauge, Tooltip } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import prettyBytes from "pretty-bytes";
import { useTheme } from "@mui/material";
import type { Diagnostics } from "@/Types/Diagnostics";
import { LAYOUT } from "@/Utils/Theme/constants";
interface MongoStatsProps {
diagnostics: Diagnostics | null;
}
export const MongoCollectionStats = ({ diagnostics }: MongoStatsProps) => {
const theme = useTheme();
const { t } = useTranslation();
const mongoStats = diagnostics?.mongoStats;
if (!mongoStats) {
return null;
}
return (
<Box
display={"grid"}
gap={theme.spacing(LAYOUT.MD)}
sx={{
gridTemplateColumns: { xs: "1fr", md: "repeat(5, 1fr)" },
"& > *": { width: "100% !important" },
}}
>
{mongoStats.collections.map((colStat) => (
<StatBox
key={colStat.name}
title={colStat.name}
subtitle={""}
>
<Stack
direction={"row"}
alignItems={"center"}
justifyContent={"space-between"}
>
<Stack>
{/* <Typography variant="body2">{`Documents: ${colStat.documentCount}`}</Typography> */}
<Typography variant="body2">
{t("pages.logs.diagnostics.mongoDBCollectionStats.documents", {
count: colStat.documentCount,
})}
</Typography>
{colStat.bucketCount !== undefined && (
<Typography variant="body2">
{t("pages.logs.diagnostics.mongoDBCollectionStats.bucketCount", {
count: colStat.bucketCount,
})}
</Typography>
)}
<Typography variant="body2">
{t("pages.logs.diagnostics.mongoDBCollectionStats.storageSize", {
size: prettyBytes(colStat.storageSize),
})}
</Typography>
<Typography variant="body2">
{t("pages.logs.diagnostics.mongoDBCollectionStats.indexSize", {
size: prettyBytes(colStat.totalIndexSize),
})}
</Typography>
<Typography variant="body2">
{t("pages.logs.diagnostics.mongoDBCollectionStats.totalSize", {
size: prettyBytes(colStat.totalSize),
})}
</Typography>
</Stack>
<Stack alignItems={"center"}>
<Tooltip
title={t("pages.logs.diagnostics.mongoDBCollectionStats.dbUsageTooltip", {
percentage: ((colStat.totalSize / mongoStats.totalSize) * 100).toFixed(
2
),
})}
placement="top"
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
},
}}
>
<Gauge progress={(colStat.totalSize / mongoStats.totalSize) * 100} />
</Tooltip>
</Stack>
</Stack>
</StatBox>
))}
</Box>
);
};
@@ -0,0 +1,74 @@
import Box from "@mui/material/Box";
import { StatBox } from "@/Components/design-elements";
import { useTranslation } from "react-i18next";
import prettyBytes from "pretty-bytes";
import { useTheme } from "@mui/material";
import type { Diagnostics } from "@/Types/Diagnostics";
import { LAYOUT } from "@/Utils/Theme/constants";
interface MongoStatsProps {
diagnostics: Diagnostics | null;
}
export const MongoStats = ({ diagnostics }: MongoStatsProps) => {
const theme = useTheme();
const { t } = useTranslation();
const mongoStats = diagnostics?.mongoStats;
if (!mongoStats) {
return null;
}
return (
<Box
display={"grid"}
gap={theme.spacing(LAYOUT.MD)}
sx={{
gridTemplateColumns: { xs: "1fr", md: "repeat(5, 1fr)" },
"& > *": { width: "100% !important" },
}}
>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.readyState")}
subtitle={mongoStats.readyState.toString()}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.readsPerSecond")}
subtitle={mongoStats.readsPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.insertsPerSecond")}
subtitle={mongoStats.insertsPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.updatesPerSecond")}
subtitle={mongoStats.updatesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.deletesPerSecond")}
subtitle={mongoStats.deletesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.writesPerSecond")}
subtitle={mongoStats.writesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.host")}
subtitle={mongoStats.host}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.port")}
subtitle={mongoStats.port.toString()}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.dbName")}
subtitle={mongoStats.dbName}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.storageSize")}
subtitle={prettyBytes(mongoStats.totalSize)}
/>
</Box>
);
};
+59 -14
View File
@@ -1,4 +1,4 @@
import { Table } from "@/Components/design-elements";
import { Table, Pagination } from "@/Components/design-elements";
import { Typography, useTheme } from "@mui/material";
import prettyMilliseconds from "pretty-ms";
import { formatTimestamp } from "@/Utils/TimeUtils";
@@ -6,14 +6,27 @@ import { formatTimestamp } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import type { QueueJobFailure, QueueJobSummary, QueueMetrics } from "@/Types/Queue";
import type { Header } from "@/Components/design-elements";
import type { TablePaginationProps } from "@mui/material/TablePagination";
type QueueJobWithId = QueueJobSummary & { id: string | number };
interface TableJobsProps {
jobs: QueueJobSummary[];
count: number;
page: number;
rowsPerPage: number;
onPageChange: TablePaginationProps["onPageChange"];
onRowsPerPageChange: TablePaginationProps["onRowsPerPageChange"];
}
export const TableJobs = ({ jobs }: TableJobsProps) => {
export const TableJobs = ({
jobs,
count,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
}: TableJobsProps) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -127,21 +140,53 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
);
},
},
{
id: "lockedBy",
content: t("pages.logs.table.headers.lockedBy"),
render: (row) => {
const lockedBy = row.lockedBy;
if (!lockedBy) {
return <Typography sx={cellSx}>-</Typography>;
}
const workerId = lockedBy.split(":")[2];
const shortWorkerId = workerId.slice(workerId.length - 8);
return (
<Typography
title={workerId}
sx={cellSx}
>
{`...${shortWorkerId}`}
</Typography>
);
},
},
];
return (
<Table
headers={headers}
data={jobsWithId}
getRowSx={(row) => ({
...(row.lockedAt && {
"& td": { backgroundColor: theme.palette.rowStatus.running },
}),
...(row.monitorActive === false && {
"& td": { backgroundColor: theme.palette.rowStatus.paused },
}),
})}
/>
<>
<Table
headers={headers}
data={jobsWithId}
getRowSx={(row) => ({
...(row.lockedAt && {
"& td": { backgroundColor: theme.palette.rowStatus.running },
}),
...(row.monitorActive === false && {
"& td": { backgroundColor: theme.palette.rowStatus.paused },
}),
})}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
itemsOnPage={jobs.length}
/>
</>
);
};
@@ -0,0 +1,94 @@
import { Table } from "@/Components/design-elements";
import { Typography, useTheme } from "@mui/material";
import { formatTimestamp } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import type { QueueWorker } from "@/Types/Queue";
import type { Header } from "@/Components/design-elements";
type QueueWorkerWithId = QueueWorker & { id: number };
interface TableWorkersProps {
queueWorkers: QueueWorkerWithId[];
}
export const TableWorkers = ({ queueWorkers }: TableWorkersProps) => {
const { t } = useTranslation();
const theme = useTheme();
const cellSx = {
whiteSpace: "nowrap" as const,
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: 220,
};
const headers: Header<QueueWorkerWithId>[] = [
{
id: "host",
content: t("pages.logs.table.headers.host"),
render: (row) => {
const host = row.workerId.split(":")[0];
return (
<Typography
fontFamily={theme.typography.fontFamilyMonospace}
sx={cellSx}
>
{host}
</Typography>
);
},
},
{
id: "processId",
content: t("pages.logs.table.headers.processId"),
render: (row) => {
const processId = row.workerId.split(":")[1];
return (
<Typography
fontFamily={theme.typography.fontFamilyMonospace}
sx={cellSx}
>
{processId}
</Typography>
);
},
},
{
id: "id",
content: t("pages.logs.table.headers.workerId"),
render: (row) => {
const workerId = row.workerId.split(":")[2];
const shortWorkerId = workerId.slice(workerId.length - 8);
return (
<Typography
fontFamily={theme.typography.fontFamilyMonospace}
title={workerId}
sx={cellSx}
>
{`...${shortWorkerId}`}
</Typography>
);
},
},
{
id: "mode",
content: t("pages.logs.table.headers.workerMode"),
render: (row) => <Typography sx={cellSx}>{row.mode}</Typography>,
},
{
id: "lastSeen",
content: t("pages.logs.table.headers.lastSeenAt"),
render: (row) => (
<Typography sx={cellSx}>{formatTimestamp(row.lastSeenAt)}</Typography>
),
},
];
return (
<Table
headers={headers}
data={queueWorkers}
/>
);
};
@@ -22,6 +22,7 @@ import type { MaintenanceWindowFormData } from "@/Validation/maintenanceWindow";
import { repeatOptions, durationUnitOptions } from "@/Validation/maintenanceWindow";
import { useMaintenanceWindowForm } from "@/Hooks/useMaintenanceWindowForm";
import { useGet, usePost, usePatch } from "@/Hooks/UseApi";
import { mutate } from "swr";
import { useParams, useNavigate } from "react-router-dom";
import type { Monitor } from "@/Types/Monitor";
import { useForm, Controller } from "react-hook-form";
@@ -85,10 +86,6 @@ const CreateMaintenanceWindowPage = () => {
repeat,
};
if (repeat === 0) {
payload.expiry = endDateTime.toISOString();
}
return payload;
};
@@ -103,6 +100,9 @@ const CreateMaintenanceWindowPage = () => {
}
if (result?.success) {
await mutate(
(key) => typeof key === "string" && key.startsWith("/maintenance-window")
);
navigate("/maintenance");
}
};
@@ -150,7 +150,8 @@ const NotificationsCreatePage = () => {
{watchedType !== "matrix" &&
watchedType !== "telegram" &&
watchedType !== "pushover" &&
watchedType !== "twilio" && (
watchedType !== "twilio" &&
watchedType !== "ntfy" && (
<ConfigBox
title={addressConfig.title}
subtitle={addressConfig.description}
@@ -174,6 +175,48 @@ const NotificationsCreatePage = () => {
}
/>
)}
{watchedType === "ntfy" && (
<ConfigBox
title={t("pages.notifications.form.ntfy.title")}
subtitle={t("pages.notifications.form.ntfy.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="address"
control={control}
defaultValue={"address" in defaults ? defaults.address : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.ntfy.optionServerUrl")}
placeholder={t("pages.notifications.form.ntfy.placeholderServerUrl")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="topic"
control={control}
defaultValue={"topic" in defaults ? defaults.topic : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.ntfy.optionTopic")}
placeholder={t("pages.notifications.form.ntfy.placeholderTopic")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "telegram" && (
<ConfigBox
title={t("pages.notifications.form.telegram.title")}
@@ -1,7 +1,14 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StrategyBadge,
ColoredLabel,
} from "@/Components/design-elements";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import { HistogramPageSpeed } from "@/Components/monitors";
import { ArrowDown, ArrowUp } from "lucide-react";
import type { Header } from "@/Components/design-elements/Table";
@@ -14,10 +21,12 @@ import { useNavigate } from "react-router-dom";
import { usePatch } from "@/Hooks/UseApi";
import type { Monitor, MonitorWithChecks } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import type { ActionMenuItem } from "@/Components/actions-menu";
export const PageSpeedMonitorsTable = ({
monitors,
tags,
refetch,
setSelectedMonitor,
sortField,
@@ -31,6 +40,7 @@ export const PageSpeedMonitorsTable = ({
setRowsPerPage,
}: {
monitors: MonitorWithChecks[];
tags: Tag[];
refetch: Function;
setSelectedMonitor: Function;
sortField: string;
@@ -179,7 +189,19 @@ export const PageSpeedMonitorsTable = ({
</Stack>
),
render: (row) => {
return row.name;
return (
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XS}
>
<Typography>{row.name}</Typography>
<StrategyBadge
strategy={row.strategy}
variant="caption"
/>
</Stack>
);
},
},
{
@@ -215,6 +237,32 @@ export const PageSpeedMonitorsTable = ({
);
},
},
{
id: "tags",
content: t("common.table.headers.tags"),
render: (row) => {
if (row.tags.length === 0) return "-";
return (
<Stack
justifyContent={"center"}
direction={isSmall ? "column" : "row"}
gap={theme.spacing(SPACING.LG)}
>
{row.tags.map((tagId) => {
const fullTag = tags.find((tag) => tag.id === tagId);
if (!fullTag) return null;
return (
<ColoredLabel
key={fullTag.id}
text={fullTag.name}
color={fullTag.color}
/>
);
})}
</Stack>
);
},
},
{
id: "actions",
+116 -6
View File
@@ -1,22 +1,32 @@
import {
MonitorBasePageWithStates,
PageSpeedKeyPriorityFallback,
ColoredLabel,
} from "@/Components/design-elements";
import { Dialog } from "@/Components/inputs";
import { Dialog, TextField } from "@/Components/inputs";
import { HeaderCreate } from "@/Components/common";
import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable";
import type { Monitor } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material";
import { SPACING } from "@/Utils/Theme/constants";
import { useTranslation } from "react-i18next";
import { useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useGet, useDelete } from "@/Hooks/UseApi";
import useDebounce from "@/Hooks/useDebounce";
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
import type { AppSettingsResponse } from "@/Types/Settings";
import { HeaderMonitorsSummary } from "@/Components/monitors";
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
const PageSpeedMonitorsPage = () => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isAdmin = useIsAdmin();
const { deleteFn, loading: isDeleting } = useDelete();
@@ -33,17 +43,50 @@ const PageSpeedMonitorsPage = () => {
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [selectedState, setSelectedState] = useState<string>("");
const [search, setSearch] = useState<string>("");
const debouncedSearch = useDebounce<string>(search, 300);
// Status: pass "up"/"down" directly to the API
// State: "active" -> true, "paused" -> false
const toFilterStatus = useMemo(() => {
if (selectedStatus === "up") return "up";
if (selectedStatus === "down") return "down";
return undefined;
}, [selectedStatus]);
const toFilterActive = useMemo(() => {
if (selectedState === "active") return "true";
if (selectedState === "paused") return "false";
return undefined;
}, [selectedState]);
// Priority: status > isActive > search > sort
const filterLookup = new Map<string | undefined, string>([
[toFilterStatus, "status"],
[toFilterActive, "isActive"],
]);
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
const field = activeFilter?.[1] || (debouncedSearch ? "name" : sortField || undefined);
const filter = activeFilter?.[0] || debouncedSearch;
const { data: tags } = useGet<Tag[]>("/tags/team");
const monitorsWithChecksUrl = useMemo(() => {
const params = new URLSearchParams();
params.append("type", "pagespeed");
selectedTags.forEach((tagId) => params.append("tags", tagId));
params.append("limit", "25");
if (page !== undefined) params.append("page", String(page));
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
if (sortField) params.append("field", sortField);
if (filter) params.append("filter", filter);
if (field) params.append("field", field);
if (sortOrder) params.append("order", sortOrder);
return `/monitors/team/with-checks?${params.toString()}`;
}, [page, rowsPerPage, sortField, sortOrder]);
}, [page, rowsPerPage, sortOrder, selectedTags, filter, field]);
const {
data: monitorsData,
@@ -75,12 +118,28 @@ const PageSpeedMonitorsPage = () => {
setSelectedMonitor(null);
};
const handleClearFilters = useCallback(() => {
setSelectedStatus("");
setSelectedState("");
setSelectedTags([]);
setSearch("");
}, []);
const hasActiveFilters = Boolean(
selectedStatus || selectedState || selectedTags.length > 0 || search
);
const effectiveTotalCount =
hasActiveFilters && (summary?.totalMonitors ?? 0) === 0
? 1
: (summary?.totalMonitors ?? 0);
return (
<MonitorBasePageWithStates
headerKey="pageSpeed"
loading={isLoading}
error={monitorsError || settingsError}
totalCount={summary?.totalMonitors ?? 0}
totalCount={effectiveTotalCount}
page="pageSpeed"
actionLink="/pagespeed/create"
priorityFallback={showApiKeyWarning ? <PageSpeedKeyPriorityFallback /> : undefined}
@@ -91,8 +150,59 @@ const PageSpeedMonitorsPage = () => {
isAdmin={isAdmin}
/>
<HeaderMonitorsSummary summary={summary} />
<Stack
direction={isSmall ? "column" : "row"}
justifyContent={isSmall ? "flex-start" : "space-between"}
gap={theme.spacing(4)}
>
<ControlsFilter
showTypes={false}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
tagOptions={tags ?? []}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
onClearFilters={handleClearFilters}
/>
<TextField
placeholder={t("pages.uptime.filters.search.placeholder")}
value={search}
onChange={(event) => {
setSearch(event.target.value);
}}
/>
</Stack>
{selectedTags.length > 0 && (
<Stack
direction={isSmall ? "column" : "row"}
alignItems={isSmall ? "flex-start" : "center"}
flexWrap="wrap"
gap={theme.spacing(SPACING.XL)}
>
<Typography color={theme.palette.text.secondary}>
{t("pages.uptime.filters.activeTags")}
</Typography>
{selectedTags.map((tagId) => {
const tag = tags?.find((t) => t.id === tagId);
if (!tag) return null;
return (
<ColoredLabel
key={tag.id}
text={tag.name}
color={tag.color}
/>
);
})}
</Stack>
)}
<PageSpeedMonitorsTable
monitors={monitors || []}
tags={tags ?? []}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
sortField={sortField}
+387 -302
View File
@@ -1,4 +1,4 @@
import { BasePage, ConfigBox } from "@/Components/design-elements";
import { BasePage, ConfigBox, StepProgress } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import { logger } from "@/Utils/logger";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
@@ -26,6 +26,7 @@ import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
import type { StatusPageFormData } from "@/Validation/statusPage";
import { statusPageStepCount, statusPageStepFieldsFor } from "@/Validation/statusPage";
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
@@ -86,13 +87,27 @@ const CreateStatusPage = () => {
defaultValues: defaults,
});
const { control, reset, handleSubmit } = form;
const { control, reset, handleSubmit, trigger } = form;
// Reset form when defaults change (from fetched data)
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
// Multi-step wizard, create only. The configure (edit) flow keeps the flat
// form, where showStep() is always true.
const [currentStep, setCurrentStep] = useState(0);
const showStep = (step: number) => !isCreate || currentStep === step;
const totalSteps = statusPageStepCount();
const handleNext = async () => {
const isValid = await trigger(statusPageStepFieldsFor(currentStep));
if (isValid) {
setCurrentStep((step) => step + 1);
}
};
const handleBack = () => setCurrentStep((step) => step - 1);
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
const computedTypes: MonitorDisplayType[] = useMemo(() => {
const selectedMonitors = (watchedMonitorIds ?? [])
@@ -136,6 +151,11 @@ const CreateStatusPage = () => {
fd.append("isPublished", String(data.isPublished));
if (data.companyName) fd.append("companyName", data.companyName);
if (data.url) fd.append("url", data.url);
if (data.customDomain !== undefined && data.customDomain !== null) {
fd.append("customDomain", data.customDomain);
} else {
fd.append("customDomain", "");
}
if (data.timezone) fd.append("timezone", data.timezone);
if (data.color) fd.append("color", data.color);
fd.append("showCharts", String(data.showCharts));
@@ -200,315 +220,355 @@ const CreateStatusPage = () => {
onSubmit={handleSubmit(onSubmit, onError)}
>
{!isCreate && <HeaderConfigStatusControls onDelete={handleDeleteClick} />}
<ConfigBox
title={t("pages.statusPages.form.access.title")}
subtitle={t("pages.statusPages.form.access.description")}
rightContent={
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.MD)}
>
<Controller
name="isPublished"
control={control}
render={({ field }) => (
<SwitchComponent
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
)}
/>
<Typography>
{t("pages.statusPages.form.access.option.published.name")}
</Typography>
</Stack>
}
/>
<ConfigBox
title={t("pages.statusPages.form.basicInfo.title")}
subtitle={t("pages.statusPages.form.basicInfo.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="companyName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.statusPages.form.basicInfo.option.name.label")}
placeholder={t(
"pages.statusPages.form.basicInfo.option.name.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.statusPages.form.basicInfo.option.url.label")}
placeholder={t(
"pages.statusPages.form.basicInfo.option.url.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.statusPages.form.monitors.title")}
subtitle={t("pages.statusPages.form.monitors.description")}
rightContent={
<Controller
name="monitors"
control={control}
render={({ field, fieldState }) => {
const selectedMonitors = field.value
.map((id: string) => monitors.find((m) => m.id === id))
.filter((m): m is Monitor => m !== undefined);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const reordered = Array.from(field.value);
const [removed] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, removed);
field.onChange(reordered);
};
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={monitors}
getOptionLabel={(option: Monitor) => option.name}
value={selectedMonitors}
onChange={(_, newValue) => {
field.onChange(newValue.map((m: Monitor) => m.id));
}}
fieldLabel={t(
"pages.statusPages.form.monitors.option.monitors.label"
)}
renderInput={(params) => (
<TextField
{...params}
placeholder={
selectedMonitors.length === 0
? t(
"pages.statusPages.form.monitors.option.monitors.placeholder"
)
: ""
}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{selectedMonitors.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="monitors-list">
{(provided) => (
<Stack
{...provided.droppableProps}
ref={provided.innerRef}
>
{selectedMonitors.map((monitor, index) => (
<Draggable
key={monitor.id}
draggableId={monitor.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}
sx={{
border: `1px solid ${theme.palette.divider}`,
cursor: "grab",
"&:active": { cursor: "grabbing" },
}}
>
<GripVertical size={20} />
<Typography
flexGrow={1}
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
field.value.filter(
(id: string) => id !== monitor.id
)
);
}}
aria-label="Remove monitor"
>
<Trash2 size={16} />
</IconButton>
</Stack>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</DragDropContext>
)}
</Stack>
);
}}
/>
}
/>
<ConfigBox
title={t("pages.statusPages.form.timezone.title")}
subtitle={t("pages.statusPages.form.timezone.description")}
rightContent={
<Controller
name="timezone"
control={control}
render={({ field }) => (
<Autocomplete
options={timezones}
getOptionLabel={(option: TimezoneOption) => option.name}
value={
timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null
}
onChange={(_, newValue: TimezoneOption | null) => {
field.onChange(newValue?._id ?? "");
}}
fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")}
renderInput={(params) => (
<TextField
{...params}
placeholder={t(
"pages.statusPages.form.timezone.option.timezone.placeholder"
)}
{isCreate && (
<StepProgress
steps={totalSteps}
current={currentStep}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.statusPages.form.access.title")}
subtitle={t("pages.statusPages.form.access.description")}
rightContent={
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.MD)}
>
<Controller
name="isPublished"
control={control}
render={({ field }) => (
<SwitchComponent
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
)}
/>
)}
/>
}
/>
<ConfigBox
title={t("pages.statusPages.form.appearance.title")}
subtitle={t("pages.statusPages.form.appearance.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Stack alignItems={"center"}>
<Typography>
{t("pages.statusPages.form.access.option.published.name")}
</Typography>
</Stack>
}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.statusPages.form.basicInfo.title")}
subtitle={t("pages.statusPages.form.basicInfo.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="logo"
name="companyName"
control={control}
render={({ field }) => (
<ImageUpload
src={field.value?.data}
onChange={(file) => {
if (file) {
field.onChange({
data: file.src,
contentType: file.file.type,
});
} else {
field.onChange(null);
}
}}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.statusPages.form.basicInfo.option.name.label")}
placeholder={t(
"pages.statusPages.form.basicInfo.option.name.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.statusPages.form.basicInfo.option.url.label")}
placeholder={t(
"pages.statusPages.form.basicInfo.option.url.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
<Controller
name="customDomain"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.statusPages.form.basicInfo.option.customDomain.label"
)}
placeholder={t(
"pages.statusPages.form.basicInfo.option.customDomain.placeholder"
)}
helperText={
fieldState.error?.message ||
t("pages.statusPages.form.basicInfo.option.customDomain.helper")
}
error={!!fieldState.error}
/>
)}
/>
</Stack>
}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.statusPages.form.monitors.title")}
subtitle={t("pages.statusPages.form.monitors.description")}
rightContent={
<Controller
name="color"
name="monitors"
control={control}
render={({ field }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={t("pages.statusPages.form.appearance.option.color.label")}
/>
)}
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.statusPages.form.theme.title")}
subtitle={t("pages.statusPages.form.theme.description")}
rightContent={
<Controller
name="theme"
control={control}
render={({ field: themeField }) => (
<Controller
name="themeMode"
control={control}
render={({ field: modeField }) => (
<ThemePicker
theme={themeField.value ?? "refined"}
themeMode={modeField.value ?? "auto"}
onThemeChange={themeField.onChange}
onThemeModeChange={modeField.onChange}
/>
)}
/>
)}
/>
}
/>
<ConfigBox
title={t("pages.statusPages.form.features.title")}
subtitle={t("pages.statusPages.form.features.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="showCharts"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
render={({ field, fieldState }) => {
const selectedMonitors = field.value
.map((id: string) => monitors.find((m) => m.id === id))
.filter((m): m is Monitor => m !== undefined);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const reordered = Array.from(field.value);
const [removed] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, removed);
field.onChange(reordered);
};
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={monitors}
getOptionLabel={(option: Monitor) => option.name}
value={selectedMonitors}
onChange={(_, newValue) => {
field.onChange(newValue.map((m: Monitor) => m.id));
}}
fieldLabel={t(
"pages.statusPages.form.monitors.option.monitors.label"
)}
renderInput={(params) => (
<TextField
{...params}
placeholder={
selectedMonitors.length === 0
? t(
"pages.statusPages.form.monitors.option.monitors.placeholder"
)
: ""
}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
}
label={t("pages.statusPages.form.features.option.showCharts.label")}
/>
)}
{selectedMonitors.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="monitors-list">
{(provided) => (
<Stack
{...provided.droppableProps}
ref={provided.innerRef}
>
{selectedMonitors.map((monitor, index) => (
<Draggable
key={monitor.id}
draggableId={monitor.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}
sx={{
border: `1px solid ${theme.palette.divider}`,
cursor: "grab",
"&:active": { cursor: "grabbing" },
}}
>
<GripVertical size={20} />
<Typography
flexGrow={1}
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
field.value.filter(
(id: string) => id !== monitor.id
)
);
}}
aria-label="Remove monitor"
>
<Trash2 size={16} />
</IconButton>
</Stack>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</DragDropContext>
)}
</Stack>
);
}}
/>
}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.statusPages.form.timezone.title")}
subtitle={t("pages.statusPages.form.timezone.description")}
rightContent={
<Controller
name="showInfrastructure"
name="timezone"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
<Autocomplete
options={timezones}
getOptionLabel={(option: TimezoneOption) => option.name}
value={
timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null
}
label={t(
"pages.statusPages.form.features.option.showInfrastructure.label"
onChange={(_, newValue: TimezoneOption | null) => {
field.onChange(newValue?._id ?? "");
}}
fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")}
renderInput={(params) => (
<TextField
{...params}
placeholder={t(
"pages.statusPages.form.timezone.option.timezone.placeholder"
)}
/>
)}
/>
)}
/>
{/* <Controller
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.statusPages.form.appearance.title")}
subtitle={t("pages.statusPages.form.appearance.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Stack alignItems={"center"}>
<Controller
name="logo"
control={control}
render={({ field }) => (
<ImageUpload
src={field.value?.data}
onChange={(file) => {
if (file) {
field.onChange({
data: file.src,
contentType: file.file.type,
});
} else {
field.onChange(null);
}
}}
/>
)}
/>
</Stack>
<Controller
name="color"
control={control}
render={({ field }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={t("pages.statusPages.form.appearance.option.color.label")}
/>
)}
/>
</Stack>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.statusPages.form.theme.title")}
subtitle={t("pages.statusPages.form.theme.description")}
rightContent={
<Controller
name="theme"
control={control}
render={({ field: themeField }) => (
<Controller
name="themeMode"
control={control}
render={({ field: modeField }) => (
<ThemePicker
theme={themeField.value ?? "refined"}
themeMode={modeField.value ?? "auto"}
onThemeChange={themeField.onChange}
onThemeModeChange={modeField.onChange}
/>
)}
/>
)}
/>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.statusPages.form.features.title")}
subtitle={t("pages.statusPages.form.features.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="showCharts"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
}
label={t("pages.statusPages.form.features.option.showCharts.label")}
/>
)}
/>
<Controller
name="showInfrastructure"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
}
label={t(
"pages.statusPages.form.features.option.showInfrastructure.label"
)}
/>
)}
/>
{/* <Controller
name="showUptimePercentage"
control={control}
render={({ field }) => (
@@ -542,21 +602,46 @@ const CreateStatusPage = () => {
/>
)}
/> */}
</Stack>
}
/>
</Stack>
}
/>
)}
<Stack
direction="row"
justifyContent="flex-end"
justifyContent={isCreate ? "space-between" : "flex-end"}
>
<Button
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
{isCreate && (
<Button
type="button"
variant="outlined"
color="secondary"
disabled={currentStep === 0}
onClick={handleBack}
>
{t("common.buttons.back")}
</Button>
)}
{isCreate && currentStep < totalSteps - 1 ? (
<Button
key="wizard-next"
type="button"
variant="contained"
color="primary"
onClick={handleNext}
>
{t("common.buttons.next")}
</Button>
) : (
<Button
key="wizard-save"
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
)}
</Stack>
<Dialog
open={isDeleteDialogOpen}
@@ -8,7 +8,8 @@ import { Settings, ExternalLink } from "lucide-react";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
import { type StatusPage } from "@/Types/StatusPage";
import { getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
interface HeaderStatusPageControlsProps {
isAdmin: boolean;
@@ -50,7 +51,7 @@ export const HeaderStatusPageControls = ({
<Typography
onClick={() => {
window.open(
`${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`,
getStatusPagePublicUrl(statusPage),
"_blank",
"noopener,noreferrer"
);
+13 -3
View File
@@ -14,6 +14,11 @@ import {
type StatusPageResponse,
type StatusPageTheme,
} from "@/Types/StatusPage";
import {
buildStatusPageApiPath,
getStatusPagePublicUrl,
isCustomDomainHost,
} from "@/Utils/statusPageUrl";
import { HeaderStatusPageControls } from "@/Pages/StatusPage/Status/Components/HeaderStatusPageControls";
import { StatusPageThemeProvider } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
import {
@@ -67,9 +72,14 @@ const StatusPageView = () => {
const isAdmin = useIsAdmin();
const location = useLocation();
const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
const onCustomDomainHost = isCustomDomainHost();
const isPublic =
onCustomDomainHost || location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const apiUrl = buildStatusPageApiPath({
url,
useCustomDomain: onCustomDomainHost,
});
const { data, isLoading, error } = useGet<StatusPageResponse>(
apiUrl,
@@ -134,7 +144,7 @@ const StatusPageView = () => {
);
}
const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
const publicUrl = getStatusPagePublicUrl(statusPage);
return (
<BasePage
loading={isLoading}
@@ -99,9 +99,9 @@ export const boldStyles = (
},
top: {
display: "flex",
alignItems: "center",
alignItems: { xs: "flex-start", md: "center" },
justifyContent: "space-between",
gap: "12px",
mb: "36px",
},
brand: {
@@ -130,7 +130,7 @@ export const boldStyles = (
hero: {
position: "relative",
borderRadius: "22px",
padding: "36px 36px 32px",
padding: { xs: "24px 20px", md: "36px 36px 32px" },
mb: "28px",
background: isDark ? heroBgDark : heroBgLight,
border: `1px solid ${tokens.border}`,
@@ -149,13 +149,14 @@ export const boldStyles = (
},
heroTitle: {
m: 0,
fontSize: 34,
fontSize: { xs: 24, md: 34 },
fontWeight: 800,
letterSpacing: "-0.02em",
lineHeight: 1.1,
color: tokens.text,
display: "flex",
alignItems: "center",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
gap: "10px",
},
heroCheck: (tone) => ({
@@ -171,9 +172,15 @@ export const boldStyles = (
}),
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "16px" },
chartSwitchWrap: {
display: "flex",
justifyContent: { xs: "center", md: "flex-end" },
mb: "16px",
},
chartSwitch: {
display: "inline-flex",
width: { xs: "100%", md: "auto" },
maxWidth: "100%",
border: `1px solid ${tokens.border}`,
borderRadius: "999px",
background: tokens.surface,
@@ -181,16 +188,20 @@ export const boldStyles = (
gap: "2px",
},
chartSwitchButton: (active) => ({
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
textAlign: "center",
border: 0,
background: active ? tokens.upSoft : "transparent",
fontFamily: "inherit",
fontSize: 11,
padding: "8px 18px",
padding: { xs: "8px 6px", md: "8px 18px" },
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.12em",
letterSpacing: { xs: "0.04em", md: "0.12em" },
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
@@ -217,13 +228,21 @@ export const boldStyles = (
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "24px",
alignItems: { xs: "start", md: "center" },
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
gridTemplateAreas: {
xs: `"name" "status" "meta"`,
md: `"name name" "meta status"`,
},
columnGap: { md: "24px" },
rowGap: { xs: "10px", md: "6px" },
p: "22px 28px",
},
cardLeft: { minWidth: 0 },
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 700,
fontSize: 17,
letterSpacing: "-0.01em",
@@ -233,10 +252,9 @@ export const boldStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
display: "flex",
gridArea: "meta",
gap: "10px",
alignItems: "center",
mt: "6px",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
},
pill: {
@@ -273,6 +291,8 @@ export const boldStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 800,
padding: "6px 14px",
@@ -296,10 +316,10 @@ export const boldStyles = (
}),
heatmap: {
padding: "0 28px 22px",
padding: { xs: "0 12px 18px", md: "0 28px 22px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
height: 48,
},
heatmapCell: (kind) => ({
@@ -312,10 +332,10 @@ export const boldStyles = (
}),
histogram: {
padding: "0 28px",
padding: { xs: "0 12px", md: "0 28px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
alignItems: "flex-end",
height: 48,
},
@@ -90,9 +90,9 @@ export const editorialStyles = (
},
top: {
display: "flex",
alignItems: "flex-end",
alignItems: { xs: "flex-start", md: "flex-end" },
justifyContent: "space-between",
gap: "12px",
mb: "48px",
borderBottom: `2px solid ${tokens.text}`,
pb: "18px",
@@ -119,7 +119,7 @@ export const editorialStyles = (
},
statusLine: {
fontSize: 22,
fontSize: { xs: 18, md: 22 },
fontWeight: 400,
m: 0,
mb: "8px",
@@ -144,26 +144,32 @@ export const editorialStyles = (
letterSpacing: "0.12em",
color: tokens.textMuted,
m: 0,
mb: "56px",
mb: { xs: "32px", md: "56px" },
},
chartSwitchWrap: {
display: "flex",
justifyContent: "flex-end",
justifyContent: { xs: "center", md: "flex-end" },
mb: "20px",
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
},
chartSwitch: {
display: "inline-flex",
width: { xs: "100%", md: "auto" },
maxWidth: "100%",
border: `1px solid ${tokens.text}`,
"& > button + button": { borderLeft: `1px solid ${tokens.text}` },
},
chartSwitchButton: (active) => ({
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
textAlign: "center",
fontFamily: "inherit",
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.2em",
padding: "6px 14px",
letterSpacing: { xs: "0.05em", md: "0.2em" },
padding: { xs: "6px 6px", md: "6px 14px" },
border: 0,
background: active ? tokens.text : "transparent",
color: active ? tokens.bg : tokens.text,
@@ -188,12 +194,20 @@ export const editorialStyles = (
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "baseline",
gap: "20px",
alignItems: { xs: "start", md: "baseline" },
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
gridTemplateAreas: {
xs: `"name" "status" "meta"`,
md: `"name name" "meta status"`,
},
columnGap: { md: "20px" },
rowGap: { xs: "10px", md: "6px" },
},
cardLeft: { minWidth: 0 },
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontSize: 20,
fontWeight: 700,
letterSpacing: "-0.015em",
@@ -203,14 +217,13 @@ export const editorialStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
gridArea: "meta",
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
fontSize: 12,
color: tokens.textMuted,
mt: "6px",
display: "flex",
gap: "12px",
flexWrap: "wrap",
alignItems: "center",
alignItems: { xs: "flex-start", md: "center" },
},
pill: {
textTransform: "uppercase",
@@ -234,6 +247,8 @@ export const editorialStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
fontSize: 10,
fontWeight: 700,
@@ -250,7 +265,7 @@ export const editorialStyles = (
mt: "16px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "2px",
gap: { xs: "1px", md: "2px" },
height: 40,
},
heatmapCell: (kind) => ({
@@ -262,7 +277,7 @@ export const editorialStyles = (
mt: "16px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "2px",
gap: { xs: "1px", md: "2px" },
alignItems: "flex-end",
height: 40,
},
@@ -11,24 +11,24 @@ export const ModernHero = ({
const { t } = useTranslation();
return (
<Box sx={styles.hero}>
<Box sx={styles.pulse(overall.tone)} />
<Box sx={styles.statusCopy}>
<Box sx={styles.heroRow}>
<Box sx={styles.pulse(overall.tone)} />
<Box
component="h1"
sx={styles.heroTitle}
>
{overall.message}
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
</Box>
);
};
@@ -17,7 +17,7 @@ export interface ModernStyles {
logoImg: SxProps<Theme>;
hero: SxProps<Theme>;
pulse: (tone: OverallTone) => SxProps<Theme>;
statusCopy: SxProps<Theme>;
heroRow: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroSub: SxProps<Theme>;
heroIcon: (tone: OverallTone) => SxProps<Theme>;
@@ -117,9 +117,9 @@ export const modernStyles = (
},
top: {
display: "flex",
alignItems: "center",
alignItems: { xs: "flex-start", md: "center" },
justifyContent: "space-between",
gap: "12px",
mb: "32px",
},
brand: {
@@ -148,11 +148,11 @@ export const modernStyles = (
hero: {
position: "relative",
borderRadius: "20px",
padding: "28px 32px",
padding: { xs: "20px", md: "28px 32px" },
mb: "24px",
display: "flex",
alignItems: "center",
gap: "20px",
flexDirection: "column",
gap: "4px",
background: tokens.surface,
border: `1px solid ${tokens.border}`,
boxShadow: cardShadow,
@@ -182,25 +182,37 @@ export const modernStyles = (
},
};
},
statusCopy: { flex: 1, minWidth: 0 },
heroRow: {
display: "flex",
alignItems: "center",
gap: { xs: "14px", md: "20px" },
},
heroTitle: {
m: 0,
mb: "4px",
fontSize: 22,
flex: 1,
minWidth: 0,
fontSize: { xs: 18, md: 22 },
fontWeight: 700,
letterSpacing: "-0.02em",
color: tokens.text,
},
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
heroIcon: (tone) => ({
display: { xs: "none", md: "flex" },
color: toneColor(tone, tokens),
display: "flex",
alignItems: "center",
flexShrink: 0,
}),
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "14px" },
chartSwitchWrap: {
display: "flex",
justifyContent: { xs: "center", md: "flex-end" },
mb: "14px",
},
chartSwitch: {
display: "inline-flex",
width: { xs: "100%", md: "auto" },
maxWidth: "100%",
border: `1px solid ${tokens.border}`,
borderRadius: "999px",
background: tokens.surface,
@@ -208,16 +220,20 @@ export const modernStyles = (
gap: "2px",
},
chartSwitchButton: (active) => ({
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
textAlign: "center",
border: 0,
background: active ? tokens.upSoft : "transparent",
fontFamily: "inherit",
fontSize: 11,
padding: "6px 16px",
padding: { xs: "6px 6px", md: "6px 16px" },
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
letterSpacing: { xs: "0.04em", md: "0.08em" },
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
@@ -256,13 +272,21 @@ export const modernStyles = (
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "20px",
alignItems: { xs: "start", md: "center" },
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
gridTemplateAreas: {
xs: `"name" "status" "meta"`,
md: `"name name" "meta status"`,
},
columnGap: { md: "20px" },
rowGap: { xs: "10px", md: "6px" },
p: "18px 24px",
},
cardLeft: { minWidth: 0 },
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 600,
fontSize: 15,
letterSpacing: "-0.005em",
@@ -272,10 +296,9 @@ export const modernStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
display: "flex",
gridArea: "meta",
gap: "10px",
alignItems: "center",
mt: "6px",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
},
pill: {
@@ -299,6 +322,8 @@ export const modernStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 700,
padding: "5px 12px",
@@ -319,10 +344,10 @@ export const modernStyles = (
}),
heatmap: {
padding: "0 24px 20px",
padding: { xs: "0 12px 16px", md: "0 24px 20px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
height: 46,
},
heatmapCell: (kind) => ({
@@ -334,10 +359,10 @@ export const modernStyles = (
}),
histogram: {
padding: "0 24px",
padding: { xs: "0 12px", md: "0 24px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
alignItems: "flex-end",
height: 46,
},
@@ -11,24 +11,24 @@ export const RefinedHero = ({
const { t } = useTranslation();
return (
<Box sx={styles.hero}>
<Box sx={styles.statusDot(overall.tone)} />
<Box sx={styles.statusCopy}>
<Box sx={styles.heroRow}>
<Box sx={styles.statusDot(overall.tone)} />
<Box
component="h1"
sx={styles.heroTitle}
>
{overall.message}
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
</Box>
);
};
@@ -17,7 +17,7 @@ export interface RefinedStyles {
company: SxProps<Theme>;
hero: SxProps<Theme>;
statusDot: (tone: OverallTone) => SxProps<Theme>;
statusCopy: SxProps<Theme>;
heroRow: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroSub: SxProps<Theme>;
heroIcon: (tone: OverallTone) => SxProps<Theme>;
@@ -102,9 +102,9 @@ export const refinedStyles = (
},
top: {
display: "flex",
alignItems: "center",
alignItems: { xs: "flex-start", md: "center" },
justifyContent: "space-between",
gap: "12px",
mb: "28px",
},
brand: {
@@ -133,10 +133,10 @@ export const refinedStyles = (
background: tokens.surface,
border: `1px solid ${tokens.border}`,
borderRadius: tokens.radius,
padding: "22px 24px",
padding: { xs: "18px 20px", md: "22px 24px" },
display: "flex",
alignItems: "center",
gap: "16px",
flexDirection: "column",
gap: "4px",
boxShadow: cardShadow,
mb: "20px",
},
@@ -148,10 +148,15 @@ export const refinedStyles = (
boxShadow: `0 0 0 4px ${toneSoft(tone, tokens)}`,
flexShrink: 0,
}),
statusCopy: { flex: 1, minWidth: 0 },
heroRow: {
display: "flex",
alignItems: "center",
gap: { xs: "12px", md: "16px" },
},
heroTitle: {
m: 0,
mb: "2px",
flex: 1,
minWidth: 0,
fontSize: 17,
fontWeight: 600,
letterSpacing: "-0.01em",
@@ -159,14 +164,21 @@ export const refinedStyles = (
},
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
heroIcon: (tone) => ({
display: { xs: "none", md: "flex" },
color: toneColor(tone, tokens),
display: "flex",
alignItems: "center",
flexShrink: 0,
}),
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "12px" },
chartSwitchWrap: {
display: "flex",
justifyContent: { xs: "center", md: "flex-end" },
mb: "12px",
},
chartSwitch: {
display: "inline-flex",
width: { xs: "100%", md: "auto" },
maxWidth: "100%",
border: `1px solid ${tokens.border}`,
borderRadius: "8px",
background: tokens.surface,
@@ -174,11 +186,15 @@ export const refinedStyles = (
gap: "2px",
},
chartSwitchButton: (active) => ({
flex: 1,
minWidth: 0,
whiteSpace: "nowrap",
textAlign: "center",
border: 0,
background: active ? tokens.upSoft : "transparent",
fontFamily: "inherit",
fontSize: 11,
padding: "5px 14px",
padding: { xs: "5px 6px", md: "5px 14px" },
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
borderRadius: "5px",
@@ -207,13 +223,21 @@ export const refinedStyles = (
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "16px",
alignItems: { xs: "start", md: "center" },
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
gridTemplateAreas: {
xs: `"name" "status" "meta"`,
md: `"name name" "meta status"`,
},
columnGap: { md: "16px" },
rowGap: { xs: "10px", md: "4px" },
p: "16px 20px",
},
cardLeft: { minWidth: 0 },
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 600,
fontSize: 14,
letterSpacing: "-0.005em",
@@ -223,10 +247,9 @@ export const refinedStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
display: "flex",
gridArea: "meta",
gap: "10px",
alignItems: "center",
mt: "4px",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
},
pill: {
@@ -251,6 +274,8 @@ export const refinedStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 600,
padding: "4px 10px",
@@ -261,10 +286,10 @@ export const refinedStyles = (
}),
heatmap: {
padding: "0 20px 16px",
padding: { xs: "0 10px 14px", md: "0 20px 16px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
height: 42,
},
heatmapCell: (kind) => ({
@@ -276,10 +301,10 @@ export const refinedStyles = (
}),
histogram: {
padding: "0 20px",
padding: { xs: "0 10px", md: "0 20px" },
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: "3px",
gap: { xs: "1px", md: "3px" },
alignItems: "flex-end",
height: 42,
},
@@ -27,6 +27,7 @@ import {
statusBadgeKey,
} from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
import { useStatusPageTheme } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
import { formatPercentage } from "@/Utils/FormatUtils";
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
@@ -101,8 +102,9 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
return (
<Box sx={styles.page}>
<Box
<Stack
component="header"
direction={{ xs: "column", md: "row" }}
sx={styles.top}
>
<HeaderSlot
@@ -112,7 +114,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
monitorCount={monitors.length}
styles={styles}
/>
</Box>
</Stack>
<HeroSlot
statusPage={statusPage}
@@ -161,6 +163,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
const showInfra = isHardware && statusPage.showInfrastructure !== false;
const showChart = !isHardware && statusPage.showCharts !== false;
const badgeTone = monitorBadgeTone(monitor.status);
const uptimePercentage = formatPercentage(monitor.uptimePercentage ?? 0);
return (
<Box
@@ -169,32 +172,39 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
sx={styles.card}
>
<Box sx={styles.cardRow}>
<Box sx={styles.cardLeft}>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<Box sx={styles.monitorMeta}>
<Box
component="span"
sx={isHardware ? styles.pillHardware : styles.pill}
>
{getMonitorTypeLabel(monitor.type, t)}
</Box>
{monitor.url && (
<Box
component="span"
sx={styles.monitorUrl}
title={monitor.url}
>
{monitor.url}
</Box>
)}
</Box>
</Box>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<Box
component="span"
sx={styles.badge(badgeTone)}
>
{t(statusBadgeKey[monitor.status])}
</Box>
<Stack
direction={{ xs: "column", md: "row" }}
sx={styles.monitorMeta}
>
<Box
component="span"
sx={isHardware ? styles.pillHardware : styles.pill}
>
{getMonitorTypeLabel(monitor.type, t)}
</Box>
<Box
component="span"
sx={{ ...styles.pill }}
>
{uptimePercentage}
</Box>
{monitor.url && (
<Box
component="span"
sx={styles.monitorUrl}
title={monitor.url}
>
{monitor.url}
</Box>
)}
</Stack>
</Box>
{showInfra && (
@@ -11,7 +11,9 @@ import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
import { getStatusPagePublicPath, getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
import { type StatusPage } from "@/Types/StatusPage";
interface StatusPagesTableProps {
data: StatusPage[];
@@ -53,8 +55,7 @@ export const StatusPagesTable = ({
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
if (row.isPublished) {
e.stopPropagation();
const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`;
window.open(url, "_blank", "noopener,noreferrer");
window.open(getStatusPagePublicUrl(row), "_blank", "noopener,noreferrer");
}
};
@@ -70,7 +71,9 @@ export const StatusPagesTable = ({
content: t("pages.statusPages.table.headers.url"),
render: (row) => {
const content = row.isPublished
? `/${row.url}`
? row.customDomain
? getStatusPagePublicPath(row)
: `/${row.url}`
: t("pages.statusPages.table.unpublished");
return (
<Stack
@@ -0,0 +1,109 @@
import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/design-elements/Table";
import { Table, ColoredLabel } from "@/Components/design-elements";
import { Pagination } from "@/Components/design-elements/Table";
import { useClientPagination } from "@/Hooks/useClientPagination";
import type { Tag } from "@/Types/Tag";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
interface TagsTableProps {
tags: Tag[];
setSelectedTag: Function;
}
export const TagsTable = ({ tags, setSelectedTag }: TagsTableProps) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
const { pagedRows, paginationProps } = useClientPagination(tags);
const getActions = (tag: Tag): ActionMenuItem[] => {
return [
{
id: 1,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/tags/configure/${tag.id}`);
},
closeMenu: true,
},
{
id: 2,
label: (
<Typography color={theme.palette.error.main}>
{t("pages.common.monitors.actions.delete")}
</Typography>
),
action: async () => {
setSelectedTag(tag);
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const headers: Header<Tag>[] = [
{
id: "name",
content: t("common.table.headers.name"),
render: (row) => {
return <Typography>{row?.name}</Typography>;
},
},
{
id: "color",
content: t("pages.tags.table.headers.color"),
render: (row) => {
return <Typography textTransform={"capitalize"}>{row?.color}</Typography>;
},
},
{
id: "chip",
content: t("pages.tags.table.headers.appearance"),
render: (row) => {
return (
<Box
display="flex"
justifyContent="center"
>
<ColoredLabel
text={row.name}
color={row.color}
/>
</Box>
);
},
},
{
id: "actions",
content: t("common.table.headers.actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
return (
<>
<Table
headers={getHeaders()}
data={pagedRows}
onRowClick={(row) => {
navigate(`/tags/configure/${row.id}`);
}}
/>
{tags.length > 0 && <Pagination {...paginationProps} />}
</>
);
};
+118
View File
@@ -0,0 +1,118 @@
import { BasePage, ConfigBox } from "@/Components/design-elements";
import { TextField, Button, ColorInput } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { mutate } from "swr";
import { useGet, usePost, usePatch } from "@/Hooks/UseApi";
import type { TagFormData } from "@/Validation/tag";
import type { Tag } from "@/Types/Tag";
import { useTranslation } from "react-i18next";
import { useTagsForm } from "@/Hooks/useTagsForm";
const TagsCreatePage = () => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const { tagId } = useParams<{ tagId: string }>();
const isEditMode = Boolean(tagId);
const { data: existingTag } = useGet<Tag>(isEditMode ? `/tags/${tagId}` : null);
const { post, loading: isSubmitting } = usePost<TagFormData, Tag>();
const { patch, loading: isPatching } = usePatch<TagFormData, Tag>();
const { schema, defaults } = useTagsForm({ data: existingTag });
const form = useForm<TagFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
});
const { control, reset, handleSubmit } = form;
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
const onSubmit = async (data: TagFormData) => {
const result = isEditMode
? await patch(`/tags/${tagId}`, data)
: await post("/tags", data);
if (result?.success) {
await mutate((key) => typeof key === "string" && key.startsWith("/tags"));
navigate("/tags");
}
};
return (
<BasePage
component="form"
onSubmit={handleSubmit(onSubmit)}
>
<ConfigBox
title={t("pages.tags.form.name.title")}
subtitle={t("pages.tags.form.name.description")}
rightContent={
<Controller
name="name"
control={control}
defaultValue={defaults.name}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.tags.form.name.optionName")}
placeholder={t("pages.tags.form.name.placeholder")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
}
/>
<ConfigBox
title={t("pages.tags.form.color.title")}
subtitle={t("pages.tags.form.color.description")}
rightContent={
<Controller
name="color"
control={control}
defaultValue={defaults.color}
render={({ field }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={t("pages.tags.form.color.optionName")}
/>
)}
/>
}
/>
<Stack
direction="row"
justifyContent="flex-end"
spacing={theme.spacing(2)}
>
<Button
loading={isSubmitting || isPatching}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
</Stack>
</BasePage>
);
};
export default TagsCreatePage;
+72
View File
@@ -0,0 +1,72 @@
import { BasePageWithStates } from "@/Components/design-elements";
import { TagsTable } from "@/Pages/Tags/components/TagsTable";
import { Dialog } from "@/Components/inputs";
import { HeaderCreate } from "@/Components/common";
import { useState } from "react";
import { useGet, useDelete } from "@/Hooks/UseApi";
import { useTranslation } from "react-i18next";
import type { Tag } from "@/Types/Tag";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
const TagsPage = () => {
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const [selectedTag, setSelectedTag] = useState<Tag | null>(null);
const isDialogOpen = Boolean(selectedTag);
const {
data: tags,
isLoading,
isValidating,
error,
refetch,
} = useGet<Tag[]>("/tags/team", {}, { keepPreviousData: true });
const { deleteFn, loading: isDeleting } = useDelete();
const handleConfirm = async () => {
if (!selectedTag) return;
await deleteFn(`/tags/${selectedTag.id}`);
setSelectedTag(null);
refetch();
};
const handleCancel = () => {
setSelectedTag(null);
};
return (
<BasePageWithStates
headerKey="tags"
page={t("pages.tags.fallback.title")}
description={t("pages.tags.fallback.description")}
loading={isLoading || isValidating}
error={!!error}
totalCount={tags?.length ?? 0}
actionButtonText={t("pages.tags.fallback.actionButton")}
actionLink="/tags/create"
>
<HeaderCreate
path="/tags/create"
isLoading={isLoading || isValidating}
isAdmin={isAdmin}
/>
<TagsTable
tags={tags ?? []}
setSelectedTag={setSelectedTag}
/>
<Dialog
open={isDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}
/>
</BasePageWithStates>
);
};
export default TagsPage;
@@ -1,4 +1,9 @@
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StatusCodeLabel,
} from "@/Components/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/design-elements";
import type { Check } from "@/Types/Check";
@@ -9,7 +14,25 @@ import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { RootState } from "@/Types/state";
import { useSelector } from "react-redux";
const getHeaders = (t: Function, uiTimezone: string) => {
export const ChecksTable = ({
checks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
checks: Check[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const headers: Header<Check>[] = [
{
id: "status",
@@ -36,32 +59,15 @@ const getHeaders = (t: Function, uiTimezone: string) => {
id: "statusCode",
content: t("pages.checks.table.headers.statusCode"),
render: (row) => {
return row.statusCode || "N/A";
return (
<StatusCodeLabel
statusCode={row.statusCode}
message={row.message}
/>
);
},
},
];
return headers;
};
export const ChecksTable = ({
checks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
checks: Check[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const headers = getHeaders(t, uiTimezone);
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
@@ -1,4 +1,9 @@
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StatusCodeLabel,
} from "@/Components/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/design-elements";
import type { FlatGeoCheck } from "@/Types/GeoCheck";
@@ -8,7 +13,24 @@ import type { RootState } from "@/Types/state";
import { useSelector } from "react-redux";
import prettyMilliseconds from "pretty-ms";
const getHeaders = (t: Function, uiTimezone: string) => {
export const GeoChecksTable = ({
geoChecks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
geoChecks: FlatGeoCheck[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const headers: Header<FlatGeoCheck>[] = [
{
id: "status",
@@ -29,7 +51,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
id: "statusCode",
content: t("pages.checks.table.headers.statusCode"),
render: (row) => {
return row.statusCode || "N/A";
return <StatusCodeLabel statusCode={row.statusCode} />;
},
},
{
@@ -51,27 +73,6 @@ const getHeaders = (t: Function, uiTimezone: string) => {
},
},
];
return headers;
};
export const GeoChecksTable = ({
geoChecks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
geoChecks: FlatGeoCheck[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const headers = getHeaders(t, uiTimezone);
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
@@ -6,6 +6,7 @@ import {
type Header,
Pagination,
StatusLabel,
ColoredLabel,
} from "@/Components/design-elements";
import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/common";
import { ActionsMenu } from "@/Components/actions-menu";
@@ -16,14 +17,20 @@ import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import { useSelector } from "react-redux";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/actions-menu";
import type { RootState } from "@/Types/state";
import { Checkbox } from "@/Components/inputs";
import type { Tag } from "@/Types/Tag";
import { SPACING } from "@/Utils/Theme/constants";
import { getUptimePercentageColor } from "@/Utils/MonitorUtils";
import { formatPercentage } from "@/Utils/FormatUtils";
interface MonitorTableProps {
monitors: Monitor[];
tags: Tag[];
refetch: () => void;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
@@ -41,6 +48,7 @@ interface MonitorTableProps {
export const MonitorTable = ({
monitors,
tags,
refetch,
setSelectedMonitor,
sortField,
@@ -60,6 +68,7 @@ export const MonitorTable = ({
const navigate = useNavigate();
const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram");
const { post } = usePost<Record<string, never>, Monitor>();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const selectedSet = new Set(selectedRows);
const isAllSelected =
@@ -252,6 +261,51 @@ export const MonitorTable = ({
}
},
},
{
id: "uptime",
content: t("pages.uptime.table.headers.uptime"),
render: (row) => {
const uptimePercentage = row.uptimePercentage ?? 0;
const formattedPercentage = formatPercentage(uptimePercentage);
const paletteKey = getUptimePercentageColor(uptimePercentage);
const color = theme.palette[paletteKey].main;
return (
<Stack alignItems={"center"}>
<ColoredLabel
text={formattedPercentage}
color={color}
/>
</Stack>
);
},
},
{
id: "tags",
content: t("common.table.headers.tags"),
render: (row) => {
if (row.tags.length === 0) return "-";
return (
<Stack
justifyContent={"center"}
direction={isSmall ? "column" : "row"}
gap={theme.spacing(SPACING.LG)}
>
{row.tags.map((tag) => {
const fullTag = tags.find((t) => t.id === tag);
if (!fullTag) return null;
return (
<ColoredLabel
key={fullTag.id}
text={fullTag.name}
color={fullTag.color}
/>
);
})}
</Stack>
);
},
},
{
id: "type",
mobileLabel: t("common.table.headers.type"),
+44 -4
View File
@@ -3,17 +3,19 @@ import {
HeaderMonitorsSummary,
BulkActionsBar,
} from "@/Components/monitors";
import { MonitorBasePageWithStates } from "@/Components/design-elements";
import { MonitorBasePageWithStates, ColoredLabel } from "@/Components/design-elements";
import { TextField, Dialog, Button } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
import { HeaderCreate } from "@/Components/common";
import { Play, Pause } from "lucide-react";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useGet, useDelete } from "@/Hooks/UseApi";
import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor";
import type { Tag } from "@/Types/Tag";
import { useState, useMemo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
@@ -22,6 +24,7 @@ import type { RootState } from "@/Types/state";
import { useTheme } from "@mui/material";
import useDebounce from "@/Hooks/useDebounce";
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
import { SPACING } from "@/Utils/Theme/constants";
const UptimeMonitorsPage = () => {
const { t } = useTranslation();
@@ -38,6 +41,7 @@ const UptimeMonitorsPage = () => {
const [selectedTypes, setSelectedTypes] = useState<MonitorType[]>([]);
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [selectedState, setSelectedState] = useState<string>("");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [page, setPage] = useState<number>(0);
const [search, setSearch] = useState<string>("");
const [sortField, setSortField] = useState<string>("");
@@ -77,12 +81,13 @@ const UptimeMonitorsPage = () => {
const effectiveTypes =
selectedTypes.length > 0
? selectedTypes
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
: ["http", "ping", "docker", "port", "game", "grpc", "websocket", "dns"];
// Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => {
const params = new URLSearchParams();
effectiveTypes.forEach((type) => params.append("type", type));
selectedTags.forEach((tagId) => params.append("tags", tagId));
params.append("limit", "25");
if (page !== undefined) params.append("page", String(page));
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
@@ -90,7 +95,7 @@ const UptimeMonitorsPage = () => {
if (field) params.append("field", field);
if (sortOrder) params.append("order", sortOrder);
return `/monitors/team/with-checks?${params.toString()}`;
}, [effectiveTypes, page, rowsPerPage, filter, field, sortOrder]);
}, [effectiveTypes, selectedTags, page, rowsPerPage, filter, field, sortOrder]);
const {
data: monitorsWithChecksData,
@@ -103,6 +108,8 @@ const UptimeMonitorsPage = () => {
{ refreshInterval: 5000, keepPreviousData: true }
);
const { data: tags } = useGet<Tag[]>("/tags/team");
const {
monitors: monitorsWithChecks,
summary,
@@ -126,6 +133,7 @@ const UptimeMonitorsPage = () => {
setSelectedTypes([]);
setSelectedStatus("");
setSelectedState("");
setSelectedTags([]);
setSearch("");
}, []);
@@ -144,7 +152,11 @@ const UptimeMonitorsPage = () => {
// Check if any filters are active
const hasActiveFilters = Boolean(
selectedTypes.length > 0 || selectedStatus || selectedState || search
selectedTypes.length > 0 ||
selectedStatus ||
selectedState ||
selectedTags.length > 0 ||
search
);
// Show empty state only when there are truly no monitors (not just filtered out)
@@ -183,6 +195,9 @@ const UptimeMonitorsPage = () => {
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
tagOptions={tags ?? []}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
onClearFilters={handleClearFilters}
/>
<TextField
@@ -194,6 +209,30 @@ const UptimeMonitorsPage = () => {
/>
</Stack>
{selectedTags.length > 0 && (
<Stack
direction={isSmall ? "column" : "row"}
alignItems={isSmall ? "flex-start" : "center"}
flexWrap="wrap"
gap={theme.spacing(SPACING.XL)}
>
<Typography color={theme.palette.text.secondary}>
{t("pages.uptime.filters.activeTags")}
</Typography>
{selectedTags.map((tagId) => {
const tag = tags?.find((t) => t.id === tagId);
if (!tag) return null;
return (
<ColoredLabel
key={tag.id}
text={tag.name}
color={tag.color}
/>
);
})}
</Stack>
)}
{!isLoading && (
<BulkActionsBar
selectedCount={selectedRows.length}
@@ -218,6 +257,7 @@ const UptimeMonitorsPage = () => {
<MonitorTable
monitors={monitorsWithChecks || []}
tags={tags || []}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
count={count || 0}
+34
View File
@@ -31,9 +31,14 @@ import CreateStatus from "@/Pages/StatusPage/Create/";
import StatusPages from "@/Pages/StatusPage/StatusPages";
import Status from "@/Pages/StatusPage/Status";
// Notifications
import Notifications from "@/Pages/Notifications";
import CreateNotifications from "@/Pages/Notifications/create";
// Tags
import Tags from "@/Pages/Tags";
import CreateTags from "@/Pages/Tags/create";
// Settings
import Account from "@/Pages/Account";
import EditUser from "@/Pages/Account/EditUser";
@@ -49,8 +54,24 @@ import Logs from "@/Pages/Logs";
import { ProtectedRoute, RoleProtectedRoute } from "@/Components/routing/RouteProtected";
import CreateMonitor from "@/Pages/CreateMonitor";
import { isCustomDomainHost } from "@/Utils/statusPageUrl";
const Routes = () => {
if (isCustomDomainHost()) {
return (
<LibRoutes>
<Route
path="/"
element={<Status />}
/>
<Route
path="*"
element={<Status />}
/>
</LibRoutes>
);
}
return (
<LibRoutes>
<Route
@@ -159,6 +180,19 @@ const Routes = () => {
path="notifications/configure/:notificationId"
element={<CreateNotifications />}
/>
<Route
path="tags"
element={<Tags />}
/>
<Route
path="tags/create"
element={<CreateTags />}
/>
<Route
path="tags/configure/:tagId"
element={<CreateTags />}
/>
<Route
path="maintenance"
+24 -12
View File
@@ -1,28 +1,17 @@
export interface Diagnostics {
osStats: OsStats;
memoryUsage: MemoryUsage;
cpuUsage: CpuUsage;
v8HeapStats: V8HeapStats;
eventLoopDelayMs: number;
uptimeMs: number;
mongoStats: MongoStats;
}
export interface OsStats {
freeMemoryBytes: number;
totalMemoryBytes: number;
}
export interface MemoryUsage {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
}
export interface CpuUsage {
userUsageMs: number;
systemUsageMs: number;
usagePercentage: number;
}
@@ -31,3 +20,26 @@ export interface V8HeapStats {
usedHeapSizeBytes: number;
heapSizeLimitBytes: number;
}
export interface MongoStats {
readyState: number;
readsPerSecond: number;
insertsPerSecond: number;
updatesPerSecond: number;
deletesPerSecond: number;
writesPerSecond: number;
host: string;
port: number;
dbName: string;
totalSize: number;
collections: CollectionStats[];
}
export interface CollectionStats {
name: string;
documentCount: number;
storageSize: number;
totalIndexSize: number;
totalSize: number;
bucketCount?: number;
}
+1 -1
View File
@@ -2,7 +2,7 @@ export type DurationUnit = "seconds" | "minutes" | "hours" | "days";
export interface MaintenanceWindow {
id: string;
monitorId: string;
monitorIds: string[];
teamId: string;
active: boolean;
name: string;
+12
View File
@@ -15,10 +15,14 @@ export const MonitorTypes = [
"game",
"grpc",
"websocket",
"dns",
"unknown",
] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const DnsRecordTypes = ["A", "AAAA", "CNAME", "MX", "TXT", "NS"] as const;
export type DnsRecordType = (typeof DnsRecordTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
@@ -40,6 +44,10 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export interface Monitor {
id: string;
userId: string;
@@ -62,6 +70,7 @@ export interface Monitor {
interval: number;
uptimePercentage?: number;
notifications: string[];
tags: string[];
secret?: string;
cpuAlertThreshold: number;
cpuAlertCounter: number;
@@ -74,10 +83,13 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
dnsServer?: string;
dnsRecordType?: DnsRecordType;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
+2
View File
@@ -9,6 +9,7 @@ export const NotificationChannels = [
"telegram",
"pushover",
"twilio",
"ntfy",
] as const;
export type NotificationChannel = (typeof NotificationChannels)[number];
@@ -25,6 +26,7 @@ export interface Notification {
accessToken?: string;
accountSid?: string;
twilioPhoneNumber?: string;
topic?: string;
createdAt: string;
updatedAt: string;
}
+18 -5
View File
@@ -1,5 +1,14 @@
import type { MonitorType } from "./Monitor";
export const QueueModes = ["primary", "worker"] as const;
export type QueueMode = (typeof QueueModes)[number];
export type QueueWorker = {
workerId: string; // hostname:pid:uuid
mode: QueueMode;
lastSeenAt: number; // epoch ms of the last heartbeat
};
export interface QueueJobFailure {
monitorId: string | number;
monitorUrl: string | null;
@@ -9,23 +18,26 @@ export interface QueueJobFailure {
failReason: string | null;
}
export interface QueueMetrics {
export type QueueMetrics = {
jobs: number;
activeJobs: number;
failingJobs: number;
jobsWithFailures: QueueJobFailure[];
totalRuns: number;
totalFailures: number;
}
workers: QueueWorker[];
};
export interface QueueJobSummary {
export type QueueJobSummary = {
monitorId: string | number;
monitorUrl: string | null;
monitorType: MonitorType | null;
monitorType: string | null;
monitorInterval: number | null;
monitorGeoInterval: number | null;
monitorActive: boolean | null;
active: boolean;
lockedBy: string | null;
lockedUntil: number | null;
lockedAt: number | null;
runCount: number;
failCount: number;
@@ -35,9 +47,10 @@ export interface QueueJobSummary {
lastRunTook: number | null;
lastFailedAt: number | null;
repeat: number | null;
}
};
export interface QueueData {
jobs: QueueJobSummary[];
count: number;
metrics: QueueMetrics;
}
+1
View File
@@ -47,6 +47,7 @@ export interface StatusPage {
type: MonitorDisplayType[];
companyName: string;
url: string;
customDomain?: string | null;
timezone?: string;
color: string;
monitors: string[];
+8
View File
@@ -0,0 +1,8 @@
export interface Tag {
id: string;
teamId: string;
name: string;
color: string;
createdAt: string;
updatedAt: string;
}
+11
View File
@@ -10,6 +10,7 @@ export const getMonitorPath = (type: MonitorType): string => {
game: "uptime",
grpc: "uptime",
websocket: "uptime",
dns: "uptime",
unknown: "uptime",
docker: "uptime",
hardware: "infrastructure",
@@ -62,6 +63,16 @@ export const getResponseTimeColor = (responseTime: number): PaletteKey => {
}
};
export const getUptimePercentageColor = (uptimePercentage: number): PaletteKey => {
if (uptimePercentage >= 0.75) {
return "success";
} else if (uptimePercentage >= 0.5) {
return "warning";
} else {
return "error";
}
};
export const getInfraGaugeColor = (val: number, theme: any) => {
if (val < 50) {
return theme.palette.success.main;
+76
View File
@@ -0,0 +1,76 @@
import type { ValueType } from "@/Components/design-elements/StatusLabel";
export const NETWORK_ERROR_CODE = 5000;
export const NETWORK_ERROR_SUBTYPES = [
"timeout",
"dns",
"refused",
"tls",
"reset",
"unknown",
] as const;
export type NetworkErrorSubtype = (typeof NETWORK_ERROR_SUBTYPES)[number];
type TranslateFn = (key: string) => string;
export const isNetworkError = (code?: number | null): boolean =>
code === NETWORK_ERROR_CODE;
const ERROR_CODE_PATTERN =
/\b(E[A-Z_]{2,}|CERT_[A-Z_]+|UNABLE_TO_VERIFY[A-Z_]*|DEPTH_ZERO_SELF_SIGNED[A-Z_]*|SELF_SIGNED[A-Z_]*)\b/;
export const extractErrorCode = (message?: string | null): string | null => {
if (!message) return null;
const match = message.match(ERROR_CODE_PATTERN);
return match ? match[1] : null;
};
export const getNetworkErrorSubtype = (message?: string | null): NetworkErrorSubtype => {
if (!message) return "unknown";
const m = message.toUpperCase();
if (m.includes("ETIMEDOUT") || m.includes("TIMEOUT") || m.includes("ESOCKETTIMEDOUT"))
return "timeout";
if (m.includes("ENOTFOUND") || m.includes("EAI_AGAIN") || m.includes("DNS"))
return "dns";
if (m.includes("ECONNREFUSED")) return "refused";
if (
m.includes("CERT_") ||
m.includes("UNABLE_TO_VERIFY") ||
m.includes("DEPTH_ZERO_SELF_SIGNED") ||
m.includes("SELF_SIGNED") ||
m.includes("TLS") ||
m.includes("SSL")
)
return "tls";
if (m.includes("ECONNRESET")) return "reset";
return "unknown";
};
export const formatStatusCode = (
code: number | null | undefined,
t: TranslateFn
): string => {
if (isNetworkError(code)) return t("common.statusCode.down");
return String(code);
};
export const getStatusCodeTooltip = (
code: number | null | undefined,
message: string | null | undefined,
t: TranslateFn
): string | null => {
if (!isNetworkError(code)) return null;
const description = t(
`common.statusCode.networkError.${getNetworkErrorSubtype(message)}`
);
const errorCode = extractErrorCode(message);
return errorCode ? `${description} (${errorCode})` : description;
};
export const getStatusCodeValueType = (code: number): ValueType => {
if (isNetworkError(code)) return "negative";
if (code < 300) return "positive";
if (code < 400) return "neutral";
return "negative";
};
+89
View File
@@ -0,0 +1,89 @@
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST as string | undefined;
const getClientOriginProtocol = (): string => {
const source =
CLIENT_HOST || (typeof window !== "undefined" ? window.location.origin : undefined);
if (!source) {
return "https:";
}
try {
return new URL(source).protocol;
} catch {
return "https:";
}
};
const buildCustomDomainOrigin = (customDomain: string): string =>
`${getClientOriginProtocol()}//${customDomain}`;
export const getAppHostname = (): string | null => {
const source =
CLIENT_HOST || (typeof window !== "undefined" ? window.location.origin : undefined);
if (!source) {
return null;
}
try {
return new URL(source).hostname.toLowerCase();
} catch {
return null;
}
};
export const isCustomDomainHost = (): boolean => {
if (typeof window === "undefined") {
return false;
}
const appHostname = getAppHostname();
if (!appHostname) {
return false;
}
return window.location.hostname.toLowerCase() !== appHostname;
};
export const getStatusPagePublicPath = (
statusPage: Pick<StatusPage, "url" | "customDomain">
): string => {
if (statusPage.customDomain) {
return buildCustomDomainOrigin(statusPage.customDomain);
}
return `${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
};
export const getStatusPagePublicUrl = (
statusPage: Pick<StatusPage, "url" | "customDomain">
): string => {
if (statusPage.customDomain) {
return buildCustomDomainOrigin(statusPage.customDomain);
}
if (typeof window !== "undefined") {
return `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
}
return `${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
};
export const buildStatusPageApiPath = (options: {
url?: string;
useCustomDomain?: boolean;
}): string | null => {
const query = "type=uptime&type=infrastructure";
if (options.useCustomDomain && typeof window !== "undefined") {
const domain = encodeURIComponent(window.location.hostname);
return `/status-page/resolve?domain=${domain}&${query}`;
}
if (options.url) {
return `/status-page/${options.url}?${query}`;
}
return null;
};
+27 -25
View File
@@ -14,30 +14,32 @@ export const durationUnitOptions = [
{ id: "days", name: "days", multiplier: 86400000 },
] as const;
export const maintenanceWindowSchema = z
.object({
name: z
.string()
.min(1, "Name is required")
.max(100, "Name must be at most 100 characters"),
repeat: z.string(),
startDate: z.string().min(1, "Start date is required"),
startTime: z.string().min(1, "Start time is required"),
duration: z.number().int().min(1, "Duration must be at least 1"),
durationUnit: z.string(),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
})
.refine(
(data) => {
const startDateTime = dayjs(data.startDate)
.set("hour", parseInt(data.startTime.split(":")[0], 10))
.set("minute", parseInt(data.startTime.split(":")[1], 10));
return startDateTime.isAfter(dayjs());
},
{
message: "Start date and time must be in the future",
path: ["startDate"],
}
);
const maintenanceWindowBaseSchema = z.object({
name: z
.string()
.min(1, "Name is required")
.max(100, "Name must be at most 100 characters"),
repeat: z.string(),
startDate: z.string().min(1, "Start date is required"),
startTime: z.string().min(1, "Start time is required"),
duration: z.number().int().min(1, "Duration must be at least 1"),
durationUnit: z.string(),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
});
export const maintenanceWindowSchema = maintenanceWindowBaseSchema.refine(
(data) => {
const startDateTime = dayjs(data.startDate)
.set("hour", parseInt(data.startTime.split(":")[0], 10))
.set("minute", parseInt(data.startTime.split(":")[1], 10));
return startDateTime.isAfter(dayjs());
},
{
message: "Start date and time must be in the future",
path: ["startDate"],
}
);
export const maintenanceWindowEditSchema = maintenanceWindowBaseSchema;
export type MaintenanceWindowFormData = z.infer<typeof maintenanceWindowSchema>;
+104 -19
View File
@@ -1,5 +1,12 @@
import { z } from "zod";
import type { FieldPath } from "react-hook-form";
import { GeoContinents } from "@/Types/GeoCheck";
import { DnsRecordTypes, PageSpeedStrategies, type MonitorType } from "@/Types/Monitor";
// Wizard step a field is validated on. Attached inline to each field below so
// the grouping lives next to the field definition; unannotated fields default
// to step 0. Read via `stepFieldsFor`.
export const monitorStepRegistry = z.registry<{ step: number }>();
// URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -11,39 +18,60 @@ const baseSchema = z.object({
.min(1, "Monitor name is required")
.max(50, "Monitor name must be at most 50 characters"),
description: z.string().optional(),
interval: z.number().min(15000, "Interval must be at least 15 seconds"),
notifications: z.array(z.string()),
interval: z
.number()
.min(15000, "Interval must be at least 15 seconds")
.register(monitorStepRegistry, { step: 1 }),
notifications: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
tags: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
statusWindowSize: z
.number({ message: "Status window size is required" })
.min(1, "Status window size must be at least 1")
.max(25, "Status window size must be at most 25"),
.max(25, "Status window size must be at most 25")
.register(monitorStepRegistry, { step: 1 }),
statusWindowThreshold: z
.number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
.max(100, "Incident percentage must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
});
// Geo-distributed check fields. Only http and ping support geo checks
// (GeoCheckSupportedTypes), so these live on those schemas rather than the base
// — that way each type's step count is exactly what its own fields declare.
const geoCheckFields = {
geoCheckEnabled: z.boolean().optional().register(monitorStepRegistry, { step: 2 }),
geoCheckLocations: z
.array(z.enum(GeoContinents))
.optional()
.register(monitorStepRegistry, { step: 2 }),
geoCheckInterval: z
.number()
.min(300000, "Interval must be at least 5 minutes")
.optional(),
});
.optional()
.register(monitorStepRegistry, { step: 2 }),
};
// HTTP monitor schema
const httpSchema = baseSchema.extend({
type: z.literal("http"),
url: urlSchema,
ignoreTlsErrors: z.boolean(),
useAdvancedMatching: z.boolean(),
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
useAdvancedMatching: z.boolean().register(monitorStepRegistry, { step: 2 }),
matchMethod: z
.enum(["equal", "include", "regex", ""])
.optional()
.register(monitorStepRegistry, { step: 2 }),
expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }),
jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }),
...geoCheckFields,
});
// Ping monitor schema
const pingSchema = baseSchema.extend({
type: z.literal("ping"),
url: z.string().min(1, "Host is required"),
...geoCheckFields,
});
// Port monitor schema
@@ -82,13 +110,14 @@ const grpcSchema = baseSchema.extend({
.min(1, "Port must be at least 1")
.max(65535, "Port must be at most 65535"),
grpcServiceName: z.string().optional(),
ignoreTlsErrors: z.boolean(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
});
// PageSpeed monitor schema
const pagespeedSchema = baseSchema.extend({
type: z.literal("pagespeed"),
url: urlSchema,
strategy: z.enum(PageSpeedStrategies),
});
// Hardware/Infrastructure monitor schema
@@ -99,19 +128,23 @@ const hardwareSchema = baseSchema.extend({
cpuAlertThreshold: z
.number()
.min(0, "CPU threshold must be at least 0")
.max(100, "CPU threshold must be at most 100"),
.max(100, "CPU threshold must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
memoryAlertThreshold: z
.number()
.min(0, "Memory threshold must be at least 0")
.max(100, "Memory threshold must be at most 100"),
.max(100, "Memory threshold must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
diskAlertThreshold: z
.number()
.min(0, "Disk threshold must be at least 0")
.max(100, "Disk threshold must be at most 100"),
.max(100, "Disk threshold must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
tempAlertThreshold: z
.number()
.min(0, "Temperature threshold must be at least 0")
.max(150, "Temperature threshold must be at most 150"),
.max(150, "Temperature threshold must be at most 150")
.register(monitorStepRegistry, { step: 1 }),
selectedDisks: z.array(z.string()),
});
@@ -119,7 +152,30 @@ const hardwareSchema = baseSchema.extend({
const websocketSchema = baseSchema.extend({
type: z.literal("websocket"),
url: z.string().min(1, "WebSocket URL is required"),
ignoreTlsErrors: z.boolean(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
});
// Hostname (FQDN) — labels of 1-63 alphanumerics/hyphens separated by dots,
// optionally prefixed with `_` for service labels (e.g. _dmarc, _imaps._tcp).
// No scheme, port, path, or whitespace. Total length ≤ 253.
const hostnameRegex =
/^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
// DNS monitor schema
const dnsSchema = baseSchema.extend({
type: z.literal("dns"),
url: z
.string()
.min(1, "Domain is required")
.regex(hostnameRegex, "Enter a valid domain (e.g. www.example.com)"),
dnsServer: z
.string()
.min(1, "DNS server is required")
.refine(
(v) => z.ipv4().safeParse(v).success || z.ipv6().safeParse(v).success,
"Enter a valid IPv4 or IPv6 address (e.g. 8.8.8.8)"
),
dnsRecordType: z.enum(DnsRecordTypes),
});
// Discriminated union of all monitor types
@@ -133,10 +189,39 @@ export const monitorSchema = z.discriminatedUnion("type", [
pagespeedSchema,
hardwareSchema,
websocketSchema,
dnsSchema,
]);
export type MonitorFormData = z.infer<typeof monitorSchema>;
const optionForType = (type: MonitorType) =>
monitorSchema.options.find((o) => o.shape.type.value === type);
// The step each field of a type is validated on, from the per-field
// `monitorStepRegistry` metadata (unannotated fields default to step 0).
const fieldStepsFor = (
type: MonitorType
): [name: FieldPath<MonitorFormData>, step: number][] =>
Object.entries(optionForType(type)?.shape ?? {}).map(([name, field]) => [
name as FieldPath<MonitorFormData>,
monitorStepRegistry.get(field)?.step ?? 0,
]);
// The selected type's fields that belong to a given wizard step. Derived from
// the schema so it can't drift from the field definitions.
export const stepFieldsFor = (
type: MonitorType,
step: number
): FieldPath<MonitorFormData>[] =>
fieldStepsFor(type)
.filter(([, fieldStep]) => fieldStep === step)
.map(([name]) => name);
// Number of wizard steps a type has, derived from the highest step its fields
// declare. A type with no step-2 fields is a 2-step form, and so on.
export const monitorStepCount = (type: MonitorType): number =>
Math.max(0, ...fieldStepsFor(type).map(([, step]) => step)) + 1;
// Type-specific schemas exported for individual use
export {
httpSchema,
+7
View File
@@ -70,6 +70,12 @@ const twilioSchema = baseSchema.extend({
twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"),
});
const ntfySchema = baseSchema.extend({
type: z.literal("ntfy"),
address: z.string().min(1, "Server URL is required").url("Please enter a valid URL"),
topic: z.string().min(1, "Topic is required"),
});
export const notificationSchema = z.discriminatedUnion("type", [
emailSchema,
slackSchema,
@@ -81,6 +87,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
telegramSchema,
pushoverSchema,
twilioSchema,
ntfySchema,
]);
export type NotificationFormData = z.infer<typeof notificationSchema>;
+68 -9
View File
@@ -1,6 +1,31 @@
import { z } from "zod";
import type { FieldPath } from "react-hook-form";
import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage";
// Wizard step a field is validated on. Attached inline to each field below so
// the grouping lives next to the field definition; unannotated fields default
// to step 0. Read via `statusPageStepFieldsFor`. Mirrors the monitor schema's
// `monitorStepRegistry` so the create-status-page wizard can't drift from the
// field definitions.
export const statusPageStepRegistry = z.registry<{ step: number }>();
const statusPageHostnameRegex =
/^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
const normalizeCustomDomainInput = (raw: string | null): string | null => {
if (raw === null) {
return null;
}
const trimmed = raw.trim().toLowerCase();
if (!trimmed) {
return null;
}
const withoutScheme = trimmed.replace(/^https?:\/\//, "");
return withoutScheme.split("/")[0]?.split(":")[0] ?? null;
};
export const statusPageSchema = z.object({
companyName: z
.string()
@@ -14,27 +39,61 @@ export const statusPageSchema = z.object({
/^[a-z0-9-]+$/,
"URL can only contain lowercase letters, numbers, and hyphens"
),
customDomain: z
.union([z.string(), z.null()])
.transform(normalizeCustomDomainInput)
.refine((domain) => domain === null || statusPageHostnameRegex.test(domain), {
message: "Enter a valid domain name (e.g. status.example.com)",
}),
timezone: z.string().optional(),
type: z
.array(z.enum(["uptime", "infrastructure"]))
.min(1, "At least one type is required"),
color: z.string().min(1, "Color is required"),
color: z.string().min(1, "Color is required").register(statusPageStepRegistry, {
step: 1,
}),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
isPublished: z.boolean(),
showCharts: z.boolean(),
showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(),
showInfrastructure: z.boolean(),
customCSS: z.string().optional(),
theme: z.enum(STATUS_PAGE_THEMES).optional(),
themeMode: z.enum(STATUS_PAGE_THEME_MODES).optional(),
showCharts: z.boolean().register(statusPageStepRegistry, { step: 1 }),
showUptimePercentage: z.boolean().register(statusPageStepRegistry, { step: 1 }),
showAdminLoginLink: z.boolean().register(statusPageStepRegistry, { step: 1 }),
showInfrastructure: z.boolean().register(statusPageStepRegistry, { step: 1 }),
customCSS: z.string().optional().register(statusPageStepRegistry, { step: 1 }),
theme: z
.enum(STATUS_PAGE_THEMES)
.optional()
.register(statusPageStepRegistry, { step: 1 }),
themeMode: z
.enum(STATUS_PAGE_THEME_MODES)
.optional()
.register(statusPageStepRegistry, { step: 1 }),
logo: z
.object({
data: z.string(),
contentType: z.string(),
})
.nullable()
.optional(),
.optional()
.register(statusPageStepRegistry, { step: 1 }),
});
export type StatusPageFormData = z.infer<typeof statusPageSchema>;
// The step each field is validated on, from the per-field
// `statusPageStepRegistry` metadata (unannotated fields default to step 0).
const fieldSteps = (): [name: FieldPath<StatusPageFormData>, step: number][] =>
Object.entries(statusPageSchema.shape).map(([name, field]) => [
name as FieldPath<StatusPageFormData>,
statusPageStepRegistry.get(field)?.step ?? 0,
]);
// The fields that belong to a given wizard step. Derived from the schema so it
// can't drift from the field definitions.
export const statusPageStepFieldsFor = (step: number): FieldPath<StatusPageFormData>[] =>
fieldSteps()
.filter(([, fieldStep]) => fieldStep === step)
.map(([name]) => name);
// Number of wizard steps, derived from the highest step its fields declare.
export const statusPageStepCount = (): number =>
Math.max(0, ...fieldSteps().map(([, step]) => step)) + 1;
+10
View File
@@ -0,0 +1,10 @@
import { z } from "zod";
export const tagSchema = z.object({
name: z.string().min(1, "Please enter a tag name"),
color: z
.string()
.regex(/^#([0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value (e.g. #13715B)"),
});
export type TagFormData = z.infer<typeof tagSchema>;
+142 -44
View File
@@ -73,7 +73,9 @@
"paused": "متوقف مؤقتاً",
"na": "غير متوفر",
"resolved": "تم الحل",
"responseTime": "وقت الاستجابة"
"responseTime": "وقت الاستجابة",
"idle": "خامل",
"running": "قيد التشغيل"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "نشط",
"responseTime": "وقت الاستجابة"
}
},
"statusCode": {
"down": "متوقف",
"networkError": {
"dns": "تعذر تحليل اسم المضيف (فشل بحث DNS).",
"refused": "رفض الخادم الاتصال (المنفذ مغلق أو الخدمة لا تستمع).",
"reset": "تمت إعادة تعيين الاتصال قبل تلقي استجابة.",
"timeout": "لم يستجب الخادم في الوقت المحدد (انتهت مهلة الطلب).",
"tls": "فشل تأكيد اتصال TLS أو SSL (مشكلة في الشهادة أو التشفير).",
"unknown": "لم يتم تلقي أي استجابة HTTP من الهدف."
}
}
},
"components": {
@@ -306,28 +319,28 @@
"common": {
"passwordRules": {
"length": {
"beginning": "",
"highlighted": ""
"beginning": "يجب أن يكون على الأقل",
"highlighted": "8 أحرف"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "يجب أن يحتوي على الأقل على",
"highlighted": "حرف صغير واحد"
},
"match": {
"beginning": "",
"highlighted": ""
"beginning": "كلمات المرور",
"highlighted": "يجب أن تتطابق"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "يجب أن يحتوي على الأقل على",
"highlighted": "رقم واحد"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "يجب أن يحتوي على الأقل على",
"highlighted": "حرف خاص واحد (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "يجب أن يحتوي على الأقل على",
"highlighted": "حرف كبير واحد"
}
},
"form": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "البنية التحتية",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "تم إيقاف {{count}} شاشة مؤقتاً",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "الشاشة المحددة متوقفة مؤقتاً بالفعل",
"alreadyPaused_other": "جميع الشاشات المحددة متوقفة مؤقتاً بالفعل",
"alreadyRunning_one": "الشاشة المحددة قيد التشغيل بالفعل",
"alreadyRunning_other": "جميع الشاشات المحددة قيد التشغيل بالفعل"
"alreadyRunning_other": "جميع الشاشات المحددة قيد التشغيل بالفعل",
"partialFailure": "تم تحديث {{successCount}} من أجهزة المراقبة، ولكن فشلت مزامنة {{failedCount}} مع قائمة انتظار الوظائف"
}
}
},
@@ -483,9 +498,9 @@
},
"matchMethod": {
"label": "طريقة المطابقة",
"equal": "",
"include": "",
"regex": ""
"equal": "يساوي",
"include": "يحتوي",
"regex": "تعبير عادي"
}
},
"title": "إعدادات متقدمة"
@@ -497,15 +512,15 @@
"label": "تكرار الفحص",
"value": {
"fifteenMinutes": "15 دقيقة",
"fifteenSeconds": "",
"fifteenSeconds": "15 ثانية",
"fiveMinutes": "5 دقائق",
"fourMinutes": "",
"oneMinute": "",
"fourMinutes": "4 دقائق",
"oneMinute": "دقيقة واحدة",
"tenMinutes": "10 دقائق",
"thirtyMinutes": "30 دقيقة",
"thirtySeconds": "",
"threeMinutes": "",
"twoMinutes": ""
"thirtySeconds": "30 ثانية",
"threeMinutes": "3 دقائق",
"twoMinutes": "دقيقتان"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "اختر اللعبة",
"placeholder": "حدد لعبة"
},
"port": {
"label": "المنفذ المراد مراقبته",
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "نقطة نهاية Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "نوع سجل DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "خادم DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "سطح المكتب",
"label": "الاستراتيجية",
"mobile": "الجوال"
}
},
"title": "الإعدادات العامة",
"description": {
"http": "أدخل الرابط أو عنوان IP للمراقبة (مثال: https://example.com/ أو 192.168.1.100) وأضف اسم عرض واضح يظهر في لوحة التحكم.",
"ping": "",
"port": "",
"docker": "",
"ping": "أدخل عنوان IP أو اسم المضيف للاتصال (على سبيل المثال، 192.168.1.100 أو example.com) وأضف اسماً واضحاً يظهر في لوحة المعلومات.",
"port": "أدخل عنوان URL أو IP للخادم ورقم المنفذ واسماً واضحاً يظهر في لوحة المعلومات.",
"docker": "أدخل اسم حاوية Docker أو المعرف. يمكنك استخدام اسم الحاوية (مثل my-app) أو معرف الحاوية (معرف كامل 64 حرف أو معرف قصير).",
"game": "أدخل عنوان IP أو اسم المضيف ورقم المنفذ للاختبار (مثل 192.168.1.100 أو example.com) واختر نوع اللعبة. تتطلب بعض الألعاب تفعيل استعلامات الخادم في ملف الإعدادات الخاص بها — يُرجى مراجعة وثائق خادم اللعبة. اطّلع على <gamedigLink>قائمة الألعاب المدعومة</gamedigLink> لمعرفة تفاصيل البروتوكول.",
"pagespeed": "تتبع أداء تحميل الصفحة ومؤشرات Core Web Vitals ودرجات التحسين لموقعك.",
"grpc": "أدخل اسم المضيف ومنفذ خادم gRPC، وحدد اختيارياً اسم خدمة لفحص الصحة، وأضف اسم عرض.",
"websocket": "أدخل رابط WebSocket للمراقبة (مثال: wss://example.com/socket) وأضف اسم عرض.",
"hardware": "راقب استخدام وحدة المعالجة المركزية والذاكرة والقرص ودرجة الحرارة لبنيتك التحتية. يجب أن يتضمن عنوان URL لنقطة نهاية Capture المسار الكامل ‎/api/v1/metrics‎ (مثال: ‎http://192.168.1.10:5001/api/v1/metrics‎). بالنسبة لمفتاح التفويض، الصق سلسلة الرمز فقط — لا تُضمّن كلمة 'Bearer'."
"hardware": "راقب استخدام وحدة المعالجة المركزية والذاكرة والقرص ودرجة الحرارة لبنيتك التحتية. يجب أن يتضمن عنوان URL لنقطة نهاية Capture المسار الكامل ‎/api/v1/metrics‎ (مثال: ‎http://192.168.1.10:5001/api/v1/metrics‎). بالنسبة لمفتاح التفويض، الصق سلسلة الرمز فقط — لا تُضمّن كلمة 'Bearer'.",
"dns": "راقب ما إذا كان خادم DNS محدد يستجيب."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "استخدم HTTP(S) لمراقبة موقعك الإلكتروني أو نقطة نهاية API.",
"optionPingDescription": "استخدم ICMP Ping لمراقبة ما إذا كان الخادم متصلاً.",
"optionPortDescription": "مراقبة ما إذا كان منفذ معين على الخادم مفتوحاً.",
"title": "النوع"
"title": "النوع",
"optionDnsDescription": "راقب ما إذا كان خادم DNS محدد يستجيب."
},
"thresholds": {
"title": "حدود التنبيه",
@@ -765,7 +802,7 @@
"headers": {
"endTime": "وقت الانتهاء",
"resolutionType": "نوع الحل",
"startTime": "",
"startTime": "وقت البدء",
"statusCode": "رمز الحالة"
},
"resolvedIncidents": "الحوادث المحلولة",
@@ -809,13 +846,13 @@
}
},
"statBoxes": {
"avgCpuTemperature": "",
"cpuFrequency": "",
"cpuLogical": "",
"cpuPhysical": "",
"avgCpuTemperature": "متوسط درجة حرارة المعالج",
"cpuFrequency": "تردد المعالج",
"cpuLogical": "المعالج (المنطقي)",
"cpuPhysical": "المعالج (الفعلي)",
"disk": "القرص",
"memory": "الذاكرة",
"os": ""
"os": "نظام التشغيل"
},
"table": {
"headers": {
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "الشبكة",
"overview": "نظرة عامة"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "آخر انتهاء في",
"lastRunTook": "مدة آخر تشغيل",
"lastFailedAt": "آخر فشل في",
"failReason": "سبب الفشل"
"failReason": "سبب الفشل",
"state": "الحالة"
}
},
"queueFallback": {
@@ -929,8 +967,8 @@
},
"table": {
"headers": {
"nextWindow": "",
"repeat": ""
"nextWindow": "النافذة التالية",
"repeat": "تكرار"
}
},
"form": {
@@ -1032,7 +1070,7 @@
},
"telegram": {
"title": "تهيئة Telegram",
"description": "",
"description": "قم بتكوين بوت Telegram الخاص بك لإرسال الإشعارات.",
"optionBotToken": "رمز البوت",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "معرّف المحادثة",
@@ -1099,6 +1137,10 @@
"header": {
"title": "مراقبات PageSpeed",
"description": "تابع Core Web Vitals ودرجات Lighthouse عبر الزمن لاكتشاف التراجع قبل أن يلاحظه المستخدمون."
},
"strategy": {
"desktop": "سطح المكتب",
"mobile": "الجوال"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "إعدادات البريد الإلكتروني",
"description": "تهيئة إعدادات البريد الإلكتروني لنظامك. يُستخدم لإرسال الإشعارات والتنبيهات.",
"descriptionTransport": "",
"descriptionTransport": "هذا يبني نقل SMTP لـ NodeMailer",
"descriptionTransportLink": "اطلع على المواصفات هنا",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "رفض الشهادات غير الصالحة: رفض الاتصالات ذات الشهادات الموقعة ذاتياً أو غير الموثوقة"
},
"displayName": {
"label": "اسم المرسل (اختياري) - الاسم الودي المعروض في حقل المرسل",
"placeholder": "إشعارات Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "وقت التشغيل",
"responseTime": "وقت الاستجابة"
},
"chart": {
"avg": "متوسط {{value}} مللي ثانية",
"downTooltip": "متوقف",
"heatmapAria": "خريطة حرارية لزمن الاستجابة",
"max": "أقصى {{value}} مللي ثانية"
},
"status": {
"breached": "تم تجاوز الحد",
"down": "متوقف",
"initializing": "قيد التهيئة",
"maintenance": "صيانة",
"paused": "متوقف مؤقتاً",
"up": "قيد التشغيل"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "بعض الأنظمة قيد الصيانة",
"partiallyPaused": "بعض الأنظمة متوقفة مؤقتًا",
"initializing": "بعض الأنظمة قيد التهيئة",
"noMonitors": "لم يتم تكوين أي شاشة"
"noMonitors": "لم يتم تكوين أي شاشة",
"monitoringSummary": "مراقبة {{count}} خدمة · يتم التحديث كل 10 ثوانٍ",
"monitoringSummary_other": "مراقبة {{count}} خدمات · يتم التحديث كل 10 ثوانٍ"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "إظهار مقاييس البنية التحتية"
}
}
},
"theme": {
"description": "اختر النمط المرئي لصفحة الحالة العامة الخاصة بك.",
"label": "السمة",
"options": {
"bold": {
"description": "خط كبير ومسافات واثقة. رائعة للصفحات التي تركز على العلامة التجارية.",
"name": "غامق"
},
"editorial": {
"description": "عناوين بخط مسرف وفواصل دقيقة. مميز وهادئ.",
"name": "تحريري"
},
"modern": {
"description": "بجودة SaaS مع نقطة حالة نابضة وبطاقات تتلاشى بنعومة.",
"name": "حديث"
},
"refined": {
"description": "نظيف واحترافي وقريب من مظهر Checkmate الكلاسيكي.",
"name": "مكرر"
}
},
"title": "سمة الصفحة"
},
"themeMode": {
"auto": "تلقائي (اتبع جهاز الزائر)",
"dark": "داكن فقط",
"label": "وضع السمة",
"light": "فاتح فقط"
}
},
"header": {
"title": "صفحات الحالة",
"description": "انشر صفحات عامة تُظهر للعملاء وأصحاب المصلحة وقت التشغيل الفعلي وتاريخ الحوادث."
},
"editorial": {
"allUp": "جميع الأنظمة تعمل بشكل طبيعي.",
"reportTitle": "تقرير الخدمة"
},
"footer": {
"poweredBy": "مشغل بواسطة"
}
},
"uptime": {
File diff suppressed because it is too large. Load diff
+130 -32
View File
@@ -73,7 +73,9 @@
"paused": "pozastaveno",
"na": "N/A",
"resolved": "Vyřešeno",
"responseTime": "Doba odezvy"
"responseTime": "Doba odezvy",
"idle": "Nečinný",
"running": "Spuštěno"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Aktivní",
"responseTime": "Doba odezvy"
}
},
"statusCode": {
"down": "Mimo provoz",
"networkError": {
"dns": "Nepodařilo se přeložit název hostitele (vyhledávání DNS selhalo).",
"refused": "Server odmítl spojení (port je uzavřený nebo služba neposlouchá).",
"reset": "Spojení bylo resetováno před přijetím odpovědi.",
"timeout": "Server neodpověděl včas (vypršel časový limit požadavku).",
"tls": "Selhal handshake TLS nebo SSL (problém s certifikátem nebo šifrováním).",
"unknown": "Z cíle nebyla obdržena žádná odpověď HTTP."
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastruktura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} monitor pozastaven",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Vybraný monitor je již pozastaven",
"alreadyPaused_other": "Všechny vybrané monitory jsou již pozastaveny",
"alreadyRunning_one": "Vybraný monitor již běží",
"alreadyRunning_other": "Všechny vybrané monitory již běží"
"alreadyRunning_other": "Všechny vybrané monitory již běží",
"partialFailure": "Bylo aktualizováno {{successCount}} monitorů, ale u {{failedCount}} se nezdařila synchronizace s frontou úloh"
}
}
},
@@ -483,9 +498,9 @@
},
"matchMethod": {
"label": "Metoda porovnání",
"equal": "",
"include": "",
"regex": ""
"equal": "Rovná se",
"include": "Obsahuje",
"regex": "Regulární výraz"
}
},
"title": "Pokročilá nastavení"
@@ -497,15 +512,15 @@
"label": "Frekvence kontroly",
"value": {
"fifteenMinutes": "15 minut",
"fifteenSeconds": "",
"fifteenSeconds": "15 sekund",
"fiveMinutes": "5 minut",
"fourMinutes": "",
"oneMinute": "",
"fourMinutes": "4 minuty",
"oneMinute": "1 minuta",
"tenMinutes": "10 minut",
"thirtyMinutes": "30 minut",
"thirtySeconds": "",
"threeMinutes": "",
"twoMinutes": ""
"thirtySeconds": "30 sekund",
"threeMinutes": "3 minuty",
"twoMinutes": "2 minuty"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "Vybrat hru",
"placeholder": "Vyberte hru"
},
"port": {
"label": "Port ke sledování",
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "Endpoint Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Typ záznamu DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS server",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Stolní počítač",
"label": "Strategie",
"mobile": "Mobil"
}
},
"title": "Obecná nastavení",
"description": {
"http": "Zadejte URL nebo IP adresu ke sledování (např. https://example.com/ nebo 192.168.1.100) a přidejte jasný zobrazovaný název, který se zobrazí na přehledu.",
"ping": "",
"port": "",
"docker": "",
"ping": "Zadejte IP adresu nebo název hostitele pro ping (např. 192.168.1.100 nebo example.com) a přidejte přehledný název, který se zobrazí na dashboardu.",
"port": "Zadejte URL nebo IP serveru, číslo portu a přehledný název, který se zobrazí na dashboardu.",
"docker": "Zadejte název nebo ID Docker kontejneru. Můžete použít buď název kontejneru (např. my-app) nebo ID kontejneru (úplné 64znakové ID nebo krátké ID).",
"game": "Zadejte IP adresu nebo název hostitele a číslo portu pro ping (např. 192.168.1.100 nebo example.com) a vyberte typ hry. Některé hry vyžadují povolení dotazů na server v jejich konfiguračním souboru — viz dokumentace vašeho herního serveru. Podrobnosti o protokolu najdete v <gamedigLink>seznamu podporovaných her</gamedigLink>.",
"pagespeed": "Sledujte výkon načítání stránky, metriky Core Web Vitals a hodnocení optimalizace vašeho webu.",
"grpc": "Zadejte název hostitele a port gRPC serveru, volitelně zadejte název služby pro kontrolu stavu a přidejte zobrazovaný název.",
"websocket": "Zadejte WebSocket URL ke sledování (např. wss://example.com/socket) a přidejte zobrazovaný název.",
"hardware": "Sledujte využití CPU, paměti, disku a teplotu vaší infrastruktury. URL endpointu Capture musí obsahovat celou cestu /api/v1/metrics (např. http://192.168.1.10:5001/api/v1/metrics). Do autorizačního klíče vložte pouze řetězec tokenu — nezahrnujte 'Bearer'."
"hardware": "Sledujte využití CPU, paměti, disku a teplotu vaší infrastruktury. URL endpointu Capture musí obsahovat celou cestu /api/v1/metrics (např. http://192.168.1.10:5001/api/v1/metrics). Do autorizačního klíče vložte pouze řetězec tokenu — nezahrnujte 'Bearer'.",
"dns": "Sledujte, zda konkrétní DNS server odpovídá."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Použijte HTTP(S) ke sledování vašeho webu nebo API koncového bodu.",
"optionPingDescription": "Použijte ICMP Ping ke sledování, zda je server online.",
"optionPortDescription": "Sledujte, zda je konkrétní port na serveru otevřen.",
"title": "Typ"
"title": "Typ",
"optionDnsDescription": "Sledujte, zda konkrétní DNS server odpovídá."
},
"thresholds": {
"title": "Prahové hodnoty upozornění",
@@ -765,7 +802,7 @@
"headers": {
"endTime": "Čas ukončení",
"resolutionType": "Typ řešení",
"startTime": "",
"startTime": "Čas zahájení",
"statusCode": "Stavový kód"
},
"resolvedIncidents": "Vyřešené incidenty",
@@ -809,13 +846,13 @@
}
},
"statBoxes": {
"avgCpuTemperature": "",
"cpuFrequency": "",
"cpuLogical": "",
"cpuPhysical": "",
"avgCpuTemperature": "Průměrná teplota CPU",
"cpuFrequency": "Frekvence CPU",
"cpuLogical": "CPU (logický)",
"cpuPhysical": "CPU (fyzický)",
"disk": "Disk",
"memory": "Paměť",
"os": ""
"os": "OS"
},
"table": {
"headers": {
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "Síť",
"overview": "Přehled"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "Naposledy dokončeno",
"lastRunTook": "Poslední běh trval",
"lastFailedAt": "Poslední selhání",
"failReason": "Důvod selhání"
"failReason": "Důvod selhání",
"state": "Stav"
}
},
"queueFallback": {
@@ -929,8 +967,8 @@
},
"table": {
"headers": {
"nextWindow": "",
"repeat": ""
"nextWindow": "Další okno",
"repeat": "Opakovat"
}
},
"form": {
@@ -1032,7 +1070,7 @@
},
"telegram": {
"title": "Konfigurace Telegramu",
"description": "",
"description": "Nakonfigurujte svého Telegram bota pro odesílání oznámení.",
"optionBotToken": "Token bota",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "ID chatu",
@@ -1099,6 +1137,10 @@
"header": {
"title": "Monitory PageSpeed",
"description": "Sledujte Core Web Vitals a skóre Lighthouse v čase a zachyťte regrese dříve než vaši uživatelé."
},
"strategy": {
"desktop": "Stolní počítač",
"mobile": "Mobil"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "Nastavení emailu",
"description": "Nastavte emailovou konfiguraci pro váš systém. Používá se k odesílání oznámení a upozornění.",
"descriptionTransport": "",
"descriptionTransport": "Toto vytvoří SMTP transport pro NodeMailer",
"descriptionTransportLink": "Zobrazit specifikace zde",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Odmítnout neplatné certifikáty: Odmítnout připojení se sebou podepsanými nebo nedůvěryhodnými certifikáty"
},
"displayName": {
"label": "Jméno odesílatele (nepovinné) - Přátelské jméno zobrazené v poli Od",
"placeholder": "Oznámení Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Dostupnost",
"responseTime": "Doba odezvy"
},
"chart": {
"avg": "průměr {{value}}ms",
"downTooltip": "Mimo provoz",
"heatmapAria": "Tepelná mapa doby odezvy",
"max": "max {{value}}ms"
},
"status": {
"breached": "Překročen limit",
"down": "Mimo provoz",
"initializing": "Inicializace",
"maintenance": "Údržba",
"paused": "Pozastaveno",
"up": "V provozu"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Některé systémy jsou v údržbě",
"partiallyPaused": "Některé systémy jsou pozastavené",
"initializing": "Některé systémy se inicializují",
"noMonitors": "Nebyly nakonfigurovány žádné monitory"
"noMonitors": "Nebyly nakonfigurovány žádné monitory",
"monitoringSummary": "Sledování {{count}} služby · Aktualizace každých 10 sekund",
"monitoringSummary_other": "Sledování {{count}} služeb · Aktualizace každých 10 sekund"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Zobrazit metriky infrastruktury"
}
}
},
"theme": {
"description": "Vyberte vizuální styl pro veřejnou stavovou stránku.",
"label": "Motiv",
"options": {
"bold": {
"description": "Velké písmo a sebevědomé rozložení. Vhodné pro stránky zaměřené na značku.",
"name": "Tučný"
},
"editorial": {
"description": "Patkové nadpisy a tenké oddělovače. Odlišný a klidný.",
"name": "Redakční"
},
"modern": {
"description": "Úroveň SaaS s pulzující stavovou tečkou a jemně mizejícími kartami.",
"name": "Moderní"
},
"refined": {
"description": "Čistý, profesionální, blízký klasickému vzhledu Checkmate.",
"name": "Vytříbený"
}
},
"title": "Motiv stránky"
},
"themeMode": {
"auto": "Automaticky (podle zařízení návštěvníka)",
"dark": "Pouze tmavý",
"label": "Režim motivu",
"light": "Pouze světlý"
}
},
"header": {
"title": "Stavové stránky",
"description": "Publikujte veřejné stránky, které zákazníkům a zúčastněným stranám zobrazují dostupnost v reálném čase a historii incidentů."
},
"editorial": {
"allUp": "Všechny systémy fungují normálně.",
"reportTitle": "Zpráva o službě"
},
"footer": {
"poweredBy": "Provozováno službou"
}
},
"uptime": {
+105 -7
View File
@@ -73,7 +73,9 @@
"paused": "pausiert",
"na": "N/A",
"resolved": "Gelöst",
"responseTime": "Antwortzeit"
"responseTime": "Antwortzeit",
"idle": "Untätig",
"running": "Läuft"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Aktiv",
"responseTime": "Antwortzeit"
}
},
"statusCode": {
"down": "Ausgefallen",
"networkError": {
"dns": "Hostname konnte nicht aufgelöst werden (DNS-Auflösung fehlgeschlagen).",
"refused": "Der Server hat die Verbindung abgelehnt (Port geschlossen oder Dienst lauscht nicht).",
"reset": "Die Verbindung wurde zurückgesetzt, bevor eine Antwort empfangen wurde.",
"timeout": "Der Server hat nicht rechtzeitig geantwortet (Zeitüberschreitung der Anfrage).",
"tls": "TLS- oder SSL-Handshake fehlgeschlagen (Zertifikats- oder Verschlüsselungsproblem).",
"unknown": "Es wurde keine HTTP-Antwort vom Ziel empfangen."
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastruktur",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} Monitor pausiert",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Der ausgewählte Monitor ist bereits pausiert",
"alreadyPaused_other": "Alle ausgewählten Monitore sind bereits pausiert",
"alreadyRunning_one": "Der ausgewählte Monitor wird bereits ausgeführt",
"alreadyRunning_other": "Alle ausgewählten Monitore werden bereits ausgeführt"
"alreadyRunning_other": "Alle ausgewählten Monitore werden bereits ausgeführt",
"partialFailure": "{{successCount}} Monitore aktualisiert, aber {{failedCount}} konnten nicht mit der Job-Warteschlange synchronisiert werden"
}
}
},
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Capture-Endpunkt",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNS-Eintragstyp",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS-Server",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Desktop",
"label": "Strategie",
"mobile": "Mobil"
}
},
"title": "Allgemeine Einstellungen",
@@ -564,7 +599,8 @@
"pagespeed": "Verfolgen Sie die Ladezeit, Core Web Vitals und Optimierungswerte Ihrer Website.",
"grpc": "Geben Sie den Hostnamen und Port des gRPC-Servers ein, optional einen Dienstnamen für den Health-Check, und fügen Sie einen Anzeigenamen hinzu.",
"websocket": "Geben Sie die zu überwachende WebSocket-URL ein (z. B. wss://example.com/socket) und fügen Sie einen Anzeigenamen hinzu.",
"hardware": "Überwachen Sie CPU-, Speicher-, Festplattenauslastung und Temperatur Ihrer Infrastruktur. Die URL des Capture-Endpunkts muss den vollständigen Pfad /api/v1/metrics enthalten (z. B. http://192.168.1.10:5001/api/v1/metrics). Fügen Sie für das Autorisierungsgeheimnis nur den Token-String ein — geben Sie 'Bearer' nicht mit an."
"hardware": "Überwachen Sie CPU-, Speicher-, Festplattenauslastung und Temperatur Ihrer Infrastruktur. Die URL des Capture-Endpunkts muss den vollständigen Pfad /api/v1/metrics enthalten (z. B. http://192.168.1.10:5001/api/v1/metrics). Fügen Sie für das Autorisierungsgeheimnis nur den Token-String ein — geben Sie 'Bearer' nicht mit an.",
"dns": "Überwachen Sie, ob ein bestimmter DNS-Server antwortet."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "HTTP(S) verwenden, um Ihre Website oder Ihren API-Endpunkt zu überwachen.",
"optionPingDescription": "ICMP-Ping verwenden, um zu überwachen, ob ein Server online ist.",
"optionPortDescription": "Überwachen, ob ein bestimmter Port auf einem Server geöffnet ist.",
"title": "Typ"
"title": "Typ",
"optionDnsDescription": "Überwachen Sie, ob ein bestimmter DNS-Server antwortet."
},
"thresholds": {
"title": "Alarmschwellenwerte",
@@ -909,7 +946,8 @@
"lastFinishedAt": "Zuletzt abgeschlossen am",
"lastRunTook": "Letzter Lauf dauerte",
"lastFailedAt": "Letzter Fehler am",
"failReason": "Fehlergrund"
"failReason": "Fehlergrund",
"state": "Status"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed-Monitore",
"description": "Verfolgen Sie Core Web Vitals und Lighthouse-Werte im Zeitverlauf, um Regressionen zu erkennen, bevor Nutzer es tun."
},
"strategy": {
"desktop": "Desktop",
"mobile": "Mobil"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Ungültige Zertifikate ablehnen: Verbindungen mit selbstsignierten oder nicht vertrauenswürdigen Zertifikaten ablehnen"
},
"displayName": {
"label": "Absendername (optional) - Anzeigename im Absenderfeld",
"placeholder": "Checkmate-Benachrichtigungen"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Verfügbarkeit",
"responseTime": "Antwortzeit"
},
"chart": {
"avg": "Ø {{value}}ms",
"downTooltip": "Ausgefallen",
"heatmapAria": "Heatmap der Antwortzeit",
"max": "max. {{value}}ms"
},
"status": {
"breached": "Schwellenwert überschritten",
"down": "Ausgefallen",
"initializing": "Initialisierung",
"maintenance": "Wartung",
"paused": "Pausiert",
"up": "Betriebsbereit"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Einige Systeme befinden sich in Wartung",
"partiallyPaused": "Einige Systeme sind pausiert",
"initializing": "Einige Systeme werden initialisiert",
"noMonitors": "Es wurden keine Monitore konfiguriert"
"noMonitors": "Es wurden keine Monitore konfiguriert",
"monitoringSummary": "Überwachung von {{count}} Dienst · Aktualisierung alle 10 Sekunden",
"monitoringSummary_other": "Überwachung von {{count}} Diensten · Aktualisierung alle 10 Sekunden"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Infrastruktur-Metriken anzeigen"
}
}
},
"theme": {
"description": "Wählen Sie den visuellen Stil für Ihre öffentliche Statusseite.",
"label": "Theme",
"options": {
"bold": {
"description": "Große Schrift und großzügige Abstände. Ideal für markenorientierte Seiten.",
"name": "Fett"
},
"editorial": {
"description": "Serifenüberschriften und feine Trennlinien. Markant und ruhig.",
"name": "Redaktionell"
},
"modern": {
"description": "SaaS-Niveau mit pulsierendem Statuspunkt und sanft verblassenden Karten.",
"name": "Modern"
},
"refined": {
"description": "Sauber, professionell, nah am klassischen Checkmate-Look.",
"name": "Verfeinert"
}
},
"title": "Seiten-Theme"
},
"themeMode": {
"auto": "Automatisch (Gerät des Besuchers folgen)",
"dark": "Nur dunkel",
"label": "Theme-Modus",
"light": "Nur hell"
}
},
"header": {
"title": "Statusseiten",
"description": "Veröffentlichen Sie öffentliche Seiten, die Kunden und Stakeholdern Echtzeit-Verfügbarkeit und Vorfallsverlauf zeigen."
},
"editorial": {
"allUp": "Alle Systeme funktionieren normal.",
"reportTitle": "Servicebericht"
},
"footer": {
"poweredBy": "Bereitgestellt von"
}
},
"uptime": {
+135 -6
View File
@@ -14,6 +14,17 @@
}
},
"appName": "Checkmate",
"statusCode": {
"down": "Down",
"networkError": {
"timeout": "The server didn't respond in time (request timed out).",
"dns": "Couldn't resolve the hostname (DNS lookup failed).",
"refused": "The server refused the connection (port closed or service not listening).",
"tls": "TLS or SSL handshake failed (certificate or encryption issue).",
"reset": "The connection was reset before a response was received.",
"unknown": "No HTTP response was received from the target."
}
},
"breadcrumbs": {
"details": "Details",
"home": "Home"
@@ -32,6 +43,8 @@
"pause": "Pause",
"resume": "Resume",
"save": "Save",
"next": "Next",
"back": "Back",
"sendInvite": "Send invite",
"test": "Test",
"testNotifications": "Test notifications",
@@ -120,7 +133,8 @@
"url": "Url",
"interval": "Interval",
"active": "Active",
"responseTime": "Response time"
"responseTime": "Response time",
"tags": "Tags"
}
}
},
@@ -173,6 +187,7 @@
"pagespeed": "Pagespeed",
"infrastructure": "Infrastructure",
"notifications": "Notifications",
"tags": "Tags",
"checks": "Checks",
"incidents": "Incidents",
"statusPages": "Status pages",
@@ -437,7 +452,8 @@
"paused_one": "{{count}} monitor paused",
"paused_other": "{{count}} monitors paused",
"resumed_one": "{{count}} monitor resumed",
"resumed_other": "{{count}} monitors resumed"
"resumed_other": "{{count}} monitors resumed",
"partialFailure": "{{successCount}} monitors updated, but {{failedCount}} failed to sync with the job queue"
},
"statBoxes": {
"activeFor": "Active for",
@@ -464,7 +480,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastructure",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
}
}
},
@@ -551,9 +568,29 @@
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
},
"strategy": {
"label": "Strategy",
"desktop": "Desktop",
"mobile": "Mobile"
},
"captureUrl": {
"label": "Capture endpoint",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsServer": {
"label": "DNS server",
"placeholder": "8.8.8.8"
},
"dnsRecordType": {
"label": "DNS record type",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
}
},
"title": "General settings",
@@ -566,7 +603,8 @@
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure. The Capture endpoint URL must include the full path /api/v1/metrics (e.g. http://192.168.1.10:5001/api/v1/metrics). For the authorization secret, paste only the token string — don't include 'Bearer'."
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure. The Capture endpoint URL must include the full path /api/v1/metrics (e.g. http://192.168.1.10:5001/api/v1/metrics). For the authorization secret, paste only the token string — don't include 'Bearer'.",
"dns": "Monitor if a specific DNS server is responding."
}
},
"ignoreTls": {
@@ -594,6 +632,10 @@
"description": "Select the notification channels you want to use",
"title": "Notifications"
},
"tags": {
"description": "Select the tags you want to apply to this monitor",
"title": "Tags"
},
"type": {
"description": "Select the type of check to perform",
"optionDockerDescription": "Use Docker to monitor if a container is running.",
@@ -603,6 +645,7 @@
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
"optionPortDescription": "Monitor if a specific port on a server is open.",
"optionDnsDescription": "Monitor if a specific DNS server is responding.",
"title": "Type"
},
"thresholds": {
@@ -842,6 +885,37 @@
"description": "Track server performance, identify bottlenecks, and ensure reliability with real-time infrastructure monitoring."
}
},
"tags": {
"header": {
"title": "Tags",
"description": "Organize your monitors with tags"
},
"fallback": {
"title": "No tags yet",
"description": "Organize your monitors with tags. Once you create a tag and assign it to a monitor, you'll be able to filter by that tag across the app and see tag details here.",
"actionButton": "Create a tag"
},
"table": {
"headers": {
"color": "Color",
"appearance": "Appearance"
}
},
"form": {
"name": {
"title": "Tag name",
"description": "Choose a name for your tag",
"optionName": "Tag name",
"placeholder": "e.g. Group 1"
},
"color": {
"title": "Tag color",
"description": "Choose a color for your tag",
"optionName": "Tag color",
"placeholder": "e.g. #13715B"
}
}
},
"logs": {
"fallback": {
"title": "No logs yet",
@@ -859,6 +933,8 @@
"logLevelSelect": {
"label": "Log level"
},
"workers": "Workers",
"workersExplainers": "Shows worker processes. Worker IDs are truncated, hover for full ID",
"jobQueue": "Job queue",
"jobQueueExplainer": "<highlight>Highlighted rows</highlight> are jobs currently being processed by a worker.",
"failedJobs": "Failed jobs",
@@ -880,6 +956,14 @@
"memoryCpu": {
"title": "Memory & CPU",
"description": "How the process is using its allocated memory and host CPU."
},
"mongoDBStats": {
"title": "MongoDB stats",
"description": "Snapshot of MongoDB metrics"
},
"mongoDBCollectionStats": {
"title": "MongoDB collection stats",
"description": "Stats for MongoDB collections"
}
},
"stats": {
@@ -889,6 +973,26 @@
"totalHeapSize": "Total heap size",
"osMemoryLimit": "OS memory limit"
},
"mongoDBStats": {
"readyState": "Ready state",
"readsPerSecond": "Reads/s",
"insertsPerSecond": "Inserts/s",
"updatesPerSecond": "Updates/s",
"deletesPerSecond": "Deletes/s",
"writesPerSecond": "Total writes/s",
"host": "Host",
"port": "Port",
"dbName": "Database name",
"storageSize": "Storage size"
},
"mongoDBCollectionStats": {
"documents": "Documents: {{count}}",
"bucketCount": "Bucket count: {{count}}",
"storageSize": "Storage size: {{size}}",
"indexSize": "Index size: {{size}}",
"totalSize": "Total size: {{size}}",
"dbUsageTooltip": "{{percentage}}% of DB storage"
},
"gauges": {
"heapAllocation": "Heap allocation",
"heapUsage": "Heap usage",
@@ -903,6 +1007,11 @@
},
"table": {
"headers": {
"host": "Host",
"processId": "Process ID",
"workerId": "Worker ID",
"workerMode": "Worker mode",
"lastSeenAt": "Last seen at",
"state": "State",
"timestamp": "Timestamp",
"level": "Level",
@@ -913,6 +1022,7 @@
"failCount": "Count",
"lastRunAt": "Last run at",
"lockedAt": "Locked at",
"lockedBy": "Locked by",
"lastFinishedAt": "Last finished at",
"lastRunTook": "Last run took",
"lastFailedAt": "Last failed",
@@ -1060,6 +1170,14 @@
"placeholderFromNumber": "+15551234567",
"optionToNumber": "To number (recipient)",
"placeholderToNumber": "+15559876543"
},
"ntfy": {
"title": "ntfy configuration",
"description": "Configure an ntfy server and topic for push notifications.",
"optionServerUrl": "Server URL",
"placeholderServerUrl": "https://ntfy.sh",
"optionTopic": "Topic",
"placeholderTopic": "checkmate-alerts"
}
},
"table": {
@@ -1073,6 +1191,10 @@
}
},
"pageSpeed": {
"strategy": {
"desktop": "Desktop",
"mobile": "Mobile"
},
"charts": {
"common": {
"cls": "Cumulative Layout Shift (CLS)",
@@ -1392,6 +1514,11 @@
"url": {
"label": "Your status page address",
"placeholder": "my-status-page"
},
"customDomain": {
"label": "Custom domain",
"placeholder": "status.example.com",
"helper": "Optional. Point a DNS record to this Checkmate instance to serve the status page at your own domain."
}
}
},
@@ -1485,11 +1612,13 @@
"filters": {
"search": {
"placeholder": "Search monitors..."
}
},
"activeTags": "Active tags"
},
"table": {
"headers": {
"responseTime": "Response time"
"responseTime": "Response time",
"uptime": "Uptime %"
}
},
"fallback": {
+105 -21
View File
@@ -47,14 +47,7 @@
"exportToJSON": "Exportar a JSON",
"importFromJSON": "Importar desde JSON",
"clearFilters": "Limpiar filtros",
"removeUser": "Eliminar usuario",
"testNotification": "Probar notificación",
"invite": "Invitar",
"copyLink": "Copiar enlace",
"regenerateToken": "Regenerar token",
"copyToken": "Copiar token",
"exportLogs": "Exportar registros",
"refresh": "Actualizar"
"removeUser": "Eliminar usuario"
},
"charts": {
"labels": {
@@ -80,7 +73,9 @@
"paused": "pausado",
"na": "N/D",
"resolved": "Resuelto",
"responseTime": "Tiempo de respuesta"
"responseTime": "Tiempo de respuesta",
"idle": "Inactivo",
"running": "En ejecución"
},
"form": {
"role": {
@@ -127,6 +122,17 @@
"active": "Activo",
"responseTime": "Tiempo de respuesta"
}
},
"statusCode": {
"down": "Caído",
"networkError": {
"dns": "No se pudo resolver el nombre de host (la búsqueda DNS falló).",
"refused": "El servidor rechazó la conexión (puerto cerrado o servicio inactivo).",
"reset": "La conexión se restableció antes de recibir una respuesta.",
"timeout": "El servidor no respondió a tiempo (tiempo de espera agotado).",
"tls": "Falló el protocolo TLS o SSL (problema de certificado o cifrado).",
"unknown": "No se recibió ninguna respuesta HTTP del destino."
}
}
},
"components": {
@@ -459,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infraestructura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} monitor pausado",
@@ -469,7 +476,8 @@
"alreadyPaused_one": "El monitor seleccionado ya está pausado",
"alreadyPaused_other": "Todos los monitores seleccionados ya están pausados",
"alreadyRunning_one": "El monitor seleccionado ya se está ejecutando",
"alreadyRunning_other": "Todos los monitores seleccionados ya se están ejecutando"
"alreadyRunning_other": "Todos los monitores seleccionados ya se están ejecutando",
"partialFailure": "Se actualizaron {{successCount}} monitores, pero {{failedCount}} fallaron al sincronizarse con la cola de trabajos"
}
}
},
@@ -559,6 +567,26 @@
"captureUrl": {
"label": "Endpoint de Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Tipo de registro DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "Servidor DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Escritorio",
"label": "Estrategia",
"mobile": "Móvil"
}
},
"title": "Configuración general",
@@ -571,7 +599,8 @@
"pagespeed": "Rastree el rendimiento de carga de página, Core Web Vitals y puntuaciones de optimización de su sitio web.",
"grpc": "Ingrese el nombre de host y puerto del servidor gRPC, opcionalmente especifique un nombre de servicio para la verificación de salud, y agregue un nombre para mostrar.",
"websocket": "Ingrese la URL de WebSocket a monitorear (p. ej., wss://example.com/socket) y agregue un nombre para mostrar.",
"hardware": "Supervisa el uso de CPU, memoria, disco y temperatura de tu infraestructura. La URL del endpoint de Capture debe incluir la ruta completa /api/v1/metrics (p. ej., http://192.168.1.10:5001/api/v1/metrics). Para el secreto de autorización, pega solo la cadena del token — no incluyas 'Bearer'."
"hardware": "Supervisa el uso de CPU, memoria, disco y temperatura de tu infraestructura. La URL del endpoint de Capture debe incluir la ruta completa /api/v1/metrics (p. ej., http://192.168.1.10:5001/api/v1/metrics). Para el secreto de autorización, pega solo la cadena del token — no incluyas 'Bearer'.",
"dns": "Monitorea si un servidor DNS específico responde."
}
},
"ignoreTls": {
@@ -608,7 +637,8 @@
"optionHttpDescription": "Use HTTP(S) para monitorear su sitio web o endpoint de API.",
"optionPingDescription": "Use ICMP Ping para monitorear si un servidor está en línea.",
"optionPortDescription": "Monitoree si un puerto específico de un servidor está abierto.",
"title": "Tipo"
"title": "Tipo",
"optionDnsDescription": "Monitorea si un servidor DNS específico responde."
},
"thresholds": {
"title": "Umbrales de alerta",
@@ -916,7 +946,8 @@
"lastFinishedAt": "Última finalización",
"lastRunTook": "Última ejecución tomó",
"lastFailedAt": "Última falla",
"failReason": "Razón de falla"
"failReason": "Razón de falla",
"state": "Estado"
}
},
"queueFallback": {
@@ -1106,6 +1137,10 @@
"header": {
"title": "Monitores de PageSpeed",
"description": "Sigue las Core Web Vitals y las puntuaciones de Lighthouse a lo largo del tiempo para detectar regresiones antes que tus usuarios."
},
"strategy": {
"desktop": "Escritorio",
"mobile": "Móvil"
}
},
"settings": {
@@ -1254,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Rechazar certificados inválidos: Rechazar conexiones con certificados autofirmados o no confiables"
},
"displayName": {
"label": "Nombre del remitente (opcional) - Nombre amigable mostrado en el campo De",
"placeholder": "Notificaciones de Checkmate"
}
}
},
@@ -1314,6 +1353,20 @@
"uptime": {
"title": "Tiempo de actividad",
"responseTime": "Tiempo de respuesta"
},
"chart": {
"avg": "prom {{value}}ms",
"downTooltip": "Caído",
"heatmapAria": "Mapa de calor del tiempo de respuesta",
"max": "máx {{value}}ms"
},
"status": {
"breached": "Umbral superado",
"down": "Caído",
"initializing": "Inicializando",
"maintenance": "Mantenimiento",
"paused": "Pausado",
"up": "Operativo"
}
},
"statusBar": {
@@ -1331,7 +1384,9 @@
"maintenance": "Algunos sistemas están en mantenimiento",
"partiallyPaused": "Algunos sistemas están en pausa",
"initializing": "Algunos sistemas se están inicializando",
"noMonitors": "No se ha configurado ningún monitor"
"noMonitors": "No se ha configurado ningún monitor",
"monitoringSummary": "Monitoreando {{count}} servicio · Actualizaciones cada 10 segundos",
"monitoringSummary_other": "Monitoreando {{count}} servicios · Actualizaciones cada 10 segundos"
},
"table": {
"headers": {
@@ -1422,18 +1477,47 @@
"label": "Mostrar métricas de infraestructura"
}
}
},
"theme": {
"description": "Elige el estilo visual para tu página de estado pública.",
"label": "Tema",
"options": {
"bold": {
"description": "Tipografía grande y espaciado confiado. Ideal para páginas centradas en la marca.",
"name": "Audaz"
},
"editorial": {
"description": "Encabezados con serifa y divisores finos. Distintivo y tranquilo.",
"name": "Editorial"
},
"modern": {
"description": "Calidad SaaS con un punto de estado pulsante y tarjetas que se desvanecen suavemente.",
"name": "Moderno"
},
"refined": {
"description": "Limpio, profesional, cercano al aspecto clásico de Checkmate.",
"name": "Refinado"
}
},
"title": "Tema de la página"
},
"themeMode": {
"auto": "Automático (seguir dispositivo del visitante)",
"dark": "Solo oscuro",
"label": "Modo de tema",
"light": "Solo claro"
}
},
"header": {
"title": "Páginas de estado",
"description": "Publica páginas públicas que muestren a tus clientes y partes interesadas la disponibilidad en tiempo real y el historial de incidentes."
},
"publicPage": {
"maintenance": {
"upcoming": "Mantenimiento programado",
"inProgress": "Mantenimiento en curso",
"completed": "Mantenimiento completado"
}
"editorial": {
"allUp": "Todos los sistemas funcionan con normalidad.",
"reportTitle": "Informe de servicio"
},
"footer": {
"poweredBy": "Desarrollado por"
}
},
"uptime": {
+142 -44
View File
@@ -73,7 +73,9 @@
"paused": "keskeytetty",
"na": "Ei saatavilla",
"resolved": "Ratkaistu",
"responseTime": "Vasteaika"
"responseTime": "Vasteaika",
"idle": "Toimeton",
"running": "Käynnissä"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Aktiivinen",
"responseTime": "Vasteaika"
}
},
"statusCode": {
"down": "Alhaalla",
"networkError": {
"dns": "Isäntänimeä ei voitu selvittää (DNS-haku epäonnistui).",
"refused": "Palvelin hylkäsi yhteyden (portti suljettu tai palvelu ei kuuntele).",
"reset": "Yhteys nollattiin ennen vastauksen vastaanottamista.",
"timeout": "Palvelin ei vastannut ajoissa (pyynnön aikakatkaisu).",
"tls": "TLS- tai SSL-kättely epäonnistui (sertifikaatti- tai salausongelma).",
"unknown": "Kohteesta ei vastaanotettu HTTP-vastausta."
}
}
},
"components": {
@@ -306,28 +319,28 @@
"common": {
"passwordRules": {
"length": {
"beginning": "",
"highlighted": ""
"beginning": "Vähintään",
"highlighted": "8 merkkiä pitkä"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "Sisällettävä vähintään",
"highlighted": "yksi pieni kirjain"
},
"match": {
"beginning": "",
"highlighted": ""
"beginning": "Salasanat",
"highlighted": "täytyy täsmätä"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "Sisällettävä vähintään",
"highlighted": "yksi numero"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "Sisällettävä vähintään",
"highlighted": "yksi erikoismerkki (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "Sisällettävä vähintään",
"highlighted": "yksi iso kirjain"
}
},
"form": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastruktuuri",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} näyttö keskeytetty",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Valittu näyttö on jo keskeytetty",
"alreadyPaused_other": "Kaikki valitut näytöt on jo keskeytetty",
"alreadyRunning_one": "Valittu näyttö on jo käynnissä",
"alreadyRunning_other": "Kaikki valitut näytöt ovat jo käynnissä"
"alreadyRunning_other": "Kaikki valitut näytöt ovat jo käynnissä",
"partialFailure": "{{successCount}} monitoria päivitettiin, mutta {{failedCount}} epäonnistui synkronoinnissa työjonon kanssa"
}
}
},
@@ -483,9 +498,9 @@
},
"matchMethod": {
"label": "Vertailutapa",
"equal": "",
"include": "",
"regex": ""
"equal": "Yhtä suuri",
"include": "Sisältää",
"regex": "Säännöllinen lauseke"
}
},
"title": "Lisäasetukset"
@@ -497,15 +512,15 @@
"label": "Tarkistusväli",
"value": {
"fifteenMinutes": "15 minuuttia",
"fifteenSeconds": "",
"fifteenSeconds": "15 sekuntia",
"fiveMinutes": "5 minuuttia",
"fourMinutes": "",
"oneMinute": "",
"fourMinutes": "4 minuuttia",
"oneMinute": "1 minuutti",
"tenMinutes": "10 minuuttia",
"thirtyMinutes": "30 minuuttia",
"thirtySeconds": "",
"threeMinutes": "",
"twoMinutes": ""
"thirtySeconds": "30 sekuntia",
"threeMinutes": "3 minuuttia",
"twoMinutes": "2 minuuttia"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "Valitse peli",
"placeholder": "Valitse peli"
},
"port": {
"label": "Monitoroitava portti",
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "Capture-päätepiste",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNS-tietuetyyppi",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS-palvelin",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Työpöytä",
"label": "Strategia",
"mobile": "Mobiili"
}
},
"title": "Yleiset asetukset",
"description": {
"http": "Syötä monitoroitava URL tai IP (esim. https://example.com/ tai 192.168.1.100) ja lisää selkeä näyttönimi, joka näkyy hallintapaneelissa.",
"ping": "",
"port": "",
"docker": "",
"ping": "Anna pingattava IP-osoite tai isäntänimi (esim. 192.168.1.100 tai example.com) ja lisää selkeä näyttönimi, joka näkyy hallintapaneelissa.",
"port": "Anna palvelimen URL tai IP, portin numero ja selkeä näyttönimi, joka näkyy hallintapaneelissa.",
"docker": "Anna Docker-kontin nimi tai tunnus. Voit käyttää joko kontin nimeä (esim. my-app) tai kontin tunnusta (täysi 64-merkkinen tunnus tai lyhyt tunnus).",
"game": "Anna IP-osoite tai isäntänimi ja portin numero pingausta varten (esim. 192.168.1.100 tai example.com) ja valitse pelityyppi. Jotkin pelit vaativat palvelinkyselyiden käyttöönoton asetustiedostossaan — tarkista pelipalvelimesi dokumentaatio. Katso protokollatiedot <gamedigLink>tuettujen pelien luettelosta</gamedigLink>.",
"pagespeed": "Seuraa sivun latausnopeutta, Core Web Vitals -arvoja ja optimointipisteitä verkkosivustollasi.",
"grpc": "Syötä gRPC-palvelimen isäntänimi ja portti, määritä halutessasi palvelun nimi terveystarkistusta varten ja lisää näyttönimi.",
"websocket": "Syötä monitoroitava WebSocket URL (esim. wss://example.com/socket) ja lisää näyttönimi.",
"hardware": "Seuraa infrastruktuurisi suorittimen, muistin, levyn käyttöä ja lämpötilaa. Capture-päätepisteen URL-osoitteen on sisällettävä koko polku /api/v1/metrics (esim. http://192.168.1.10:5001/api/v1/metrics). Liitä valtuutusavaimeen vain tokenin merkkijono — älä sisällytä sanaa 'Bearer'."
"hardware": "Seuraa infrastruktuurisi suorittimen, muistin, levyn käyttöä ja lämpötilaa. Capture-päätepisteen URL-osoitteen on sisällettävä koko polku /api/v1/metrics (esim. http://192.168.1.10:5001/api/v1/metrics). Liitä valtuutusavaimeen vain tokenin merkkijono — älä sisällytä sanaa 'Bearer'.",
"dns": "Tarkkaile, vastaako tietty DNS-palvelin."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Käytä HTTP(S)-protokollaa verkkosivustosi tai API-päätepisteen monitorointiin.",
"optionPingDescription": "Käytä ICMP Ping -komentoa monitoroimaan, onko palvelin verkossa.",
"optionPortDescription": "Monitoroi, onko tietty palvelimen portti auki.",
"title": "Tyyppi"
"title": "Tyyppi",
"optionDnsDescription": "Tarkkaile, vastaako tietty DNS-palvelin."
},
"thresholds": {
"title": "Hälytysrajat",
@@ -765,7 +802,7 @@
"headers": {
"endTime": "Päättymisaika",
"resolutionType": "Ratkaisun tyyppi",
"startTime": "",
"startTime": "Aloitusaika",
"statusCode": "Tilakoodi"
},
"resolvedIncidents": "Ratkaistut häiriöt",
@@ -809,13 +846,13 @@
}
},
"statBoxes": {
"avgCpuTemperature": "",
"cpuFrequency": "",
"cpuLogical": "",
"cpuPhysical": "",
"avgCpuTemperature": "Keskimääräinen CPU-lämpötila",
"cpuFrequency": "CPU-taajuus",
"cpuLogical": "CPU (looginen)",
"cpuPhysical": "CPU (fyysinen)",
"disk": "Levy",
"memory": "Muisti",
"os": ""
"os": "Käyttöjärjestelmä"
},
"table": {
"headers": {
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "Verkko",
"overview": "Yleiskatsaus"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "Viimeisin valmistuminen",
"lastRunTook": "Viimeisin suoritus kesti",
"lastFailedAt": "Viimeisin epäonnistuminen",
"failReason": "Epäonnistumisen syy"
"failReason": "Epäonnistumisen syy",
"state": "Tila"
}
},
"queueFallback": {
@@ -929,8 +967,8 @@
},
"table": {
"headers": {
"nextWindow": "",
"repeat": ""
"nextWindow": "Seuraava ikkuna",
"repeat": "Toista"
}
},
"form": {
@@ -1032,7 +1070,7 @@
},
"telegram": {
"title": "Telegram-määritykset",
"description": "",
"description": "Määritä Telegram-bottisi lähettämään ilmoituksia.",
"optionBotToken": "Bottitunniste",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "Chat-tunnus",
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed-monitorit",
"description": "Seuraa Core Web Vitals -mittareita ja Lighthouse-pisteitä ajan mittaan ja huomaa heikennykset ennen käyttäjiä."
},
"strategy": {
"desktop": "Työpöytä",
"mobile": "Mobiili"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "Sähköpostiasetukset",
"description": "Määritä järjestelmäsi sähköpostiasetukset. Niitä käytetään ilmoitusten ja hälytysten lähettämiseen.",
"descriptionTransport": "",
"descriptionTransport": "Tämä rakentaa SMTP-kuljetuksen NodeMailerille",
"descriptionTransportLink": "Katso tekniset tiedot täältä",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Hylkää virheelliset varmenteet: Hylkää yhteydet itseallekirjoitetuilla tai ei-luotetuilla varmenteilla"
},
"displayName": {
"label": "Lähettäjän nimi (valinnainen) - Lähettäjäkentässä näkyvä ystävällinen nimi",
"placeholder": "Checkmate-ilmoitukset"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Käytettävyys",
"responseTime": "Vasteaika"
},
"chart": {
"avg": "ka {{value}}ms",
"downTooltip": "Alhaalla",
"heatmapAria": "Vasteajan lämpökartta",
"max": "max {{value}}ms"
},
"status": {
"breached": "Kynnys ylittyi",
"down": "Alhaalla",
"initializing": "Alustetaan",
"maintenance": "Huolto",
"paused": "Keskeytetty",
"up": "Toiminnassa"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Jotkin järjestelmät ovat huollossa",
"partiallyPaused": "Jotkin järjestelmät on keskeytetty",
"initializing": "Jotkin järjestelmät alustuvat",
"noMonitors": "Yhtään monitoria ei ole määritetty"
"noMonitors": "Yhtään monitoria ei ole määritetty",
"monitoringSummary": "Tarkkaillaan {{count}} palvelua · Päivitykset 10 sekunnin välein",
"monitoringSummary_other": "Tarkkaillaan {{count}} palvelua · Päivitykset 10 sekunnin välein"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Näytä infrastruktuurin mittarit"
}
}
},
"theme": {
"description": "Valitse julkisen tilasivusi visuaalinen tyyli.",
"label": "Teema",
"options": {
"bold": {
"description": "Suuri typografia ja itsevarma välistys. Sopii brändilähtöisille sivuille.",
"name": "Rohkea"
},
"editorial": {
"description": "Antiikvaotsikot ja ohuet erottimet. Erottuva ja rauhallinen.",
"name": "Toimituksellinen"
},
"modern": {
"description": "SaaS-tasoa sykkivällä tilapisteellä ja pehmeästi haalistuvilla korteilla.",
"name": "Moderni"
},
"refined": {
"description": "Siisti, ammattimainen, lähellä klassista Checkmate-ilmettä.",
"name": "Hienostunut"
}
},
"title": "Sivun teema"
},
"themeMode": {
"auto": "Automaattinen (seuraa vierailijan laitetta)",
"dark": "Vain tumma",
"label": "Teematila",
"light": "Vain vaalea"
}
},
"header": {
"title": "Tilasivut",
"description": "Julkaise julkisia sivuja, jotka näyttävät asiakkaille ja sidosryhmille reaaliaikaisen käytettävyyden ja tapaushistorian."
},
"editorial": {
"allUp": "Kaikki järjestelmät toimivat normaalisti.",
"reportTitle": "Palveluraportti"
},
"footer": {
"poweredBy": "Tarjoaa"
}
},
"uptime": {
+108 -10
View File
@@ -73,7 +73,9 @@
"paused": "en pause",
"na": "N/A",
"resolved": "Résolu",
"responseTime": "Temps de réponse"
"responseTime": "Temps de réponse",
"idle": "Inactif",
"running": "En cours d'exécution"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Actif",
"responseTime": "Temps de réponse"
}
},
"statusCode": {
"down": "Hors service",
"networkError": {
"dns": "Impossible de résoudre le nom d'hôte (échec de la recherche DNS).",
"refused": "Le serveur a refusé la connexion (port fermé ou service non à l'écoute).",
"reset": "La connexion a été réinitialisée avant la réception d'une réponse.",
"timeout": "Le serveur n'a pas répondu à temps (délai d'attente dépassé).",
"tls": "Échec de la négociation TLS ou SSL (problème de certificat ou de chiffrement).",
"unknown": "Aucune réponse HTTP reçue de la cible."
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastructure",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} moniteur en pause",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Le moniteur sélectionné est déjà en pause",
"alreadyPaused_other": "Tous les moniteurs sélectionnés sont déjà en pause",
"alreadyRunning_one": "Le moniteur sélectionné est déjà en cours d'exécution",
"alreadyRunning_other": "Tous les moniteurs sélectionnés sont déjà en cours d'exécution"
"alreadyRunning_other": "Tous les moniteurs sélectionnés sont déjà en cours d'exécution",
"partialFailure": "{{successCount}} moniteurs mis à jour, mais {{failedCount}} n'ont pas pu se synchroniser avec la file d'attente des tâches"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "Choisir un jeu",
"placeholder": "Sélectionner un jeu"
},
"port": {
"label": "Port à surveiller",
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Point de terminaison Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Type d'enregistrement DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "Serveur DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Bureau",
"label": "Stratégie",
"mobile": "Mobile"
}
},
"title": "Paramètres généraux",
@@ -564,7 +599,8 @@
"pagespeed": "Suivez les performances de chargement, les Core Web Vitals et les scores d'optimisation de votre site web.",
"grpc": "Saisissez le nom d'hôte et le port du serveur gRPC, spécifiez éventuellement un nom de service pour le bilan de santé, et ajoutez un nom d'affichage.",
"websocket": "Saisissez l'URL WebSocket à surveiller (ex. wss://example.com/socket) et ajoutez un nom d'affichage.",
"hardware": "Surveillez l'utilisation du processeur, de la mémoire, du disque et la température de votre infrastructure. L'URL du point de terminaison Capture doit inclure le chemin complet /api/v1/metrics (par ex. http://192.168.1.10:5001/api/v1/metrics). Pour le secret d'autorisation, collez uniquement la chaîne du jeton — n'incluez pas 'Bearer'."
"hardware": "Surveillez l'utilisation du processeur, de la mémoire, du disque et la température de votre infrastructure. L'URL du point de terminaison Capture doit inclure le chemin complet /api/v1/metrics (par ex. http://192.168.1.10:5001/api/v1/metrics). Pour le secret d'autorisation, collez uniquement la chaîne du jeton — n'incluez pas 'Bearer'.",
"dns": "Surveillez si un serveur DNS spécifique répond."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Utilisez HTTP(S) pour surveiller votre site web ou point de terminaison API.",
"optionPingDescription": "Utilisez le Ping ICMP pour surveiller si un serveur est en ligne.",
"optionPortDescription": "Surveillez si un port spécifique sur un serveur est ouvert.",
"title": "Type"
"title": "Type",
"optionDnsDescription": "Surveillez si un serveur DNS spécifique répond."
},
"thresholds": {
"title": "Seuils d'alerte",
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "Réseau",
"overview": "Aperçu"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "Dernière fin",
"lastRunTook": "Durée dernière exécution",
"lastFailedAt": "Dernier échec",
"failReason": "Raison de l'échec"
"failReason": "Raison de l'échec",
"state": "État"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "Moniteurs PageSpeed",
"description": "Suivez les Core Web Vitals et les scores Lighthouse au fil du temps pour détecter les régressions avant vos utilisateurs."
},
"strategy": {
"desktop": "Bureau",
"mobile": "Mobile"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Rejeter les certificats invalides : refuser les connexions avec des certificats auto-signés ou non fiables"
},
"displayName": {
"label": "Nom de l'expéditeur (facultatif) - Nom convivial affiché dans le champ De",
"placeholder": "Notifications Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Disponibilité",
"responseTime": "Temps de réponse"
},
"chart": {
"avg": "moy {{value}}ms",
"downTooltip": "Hors service",
"heatmapAria": "Carte thermique des temps de réponse",
"max": "max {{value}}ms"
},
"status": {
"breached": "Seuil dépassé",
"down": "Hors service",
"initializing": "Initialisation",
"maintenance": "Maintenance",
"paused": "En pause",
"up": "Opérationnel"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Certains systèmes sont en maintenance",
"partiallyPaused": "Certains systèmes sont en pause",
"initializing": "Certains systèmes sont en cours d'initialisation",
"noMonitors": "Aucun moniteur n'a été configuré"
"noMonitors": "Aucun moniteur n'a été configuré",
"monitoringSummary": "Surveillance de {{count}} service · Mises à jour toutes les 10 secondes",
"monitoringSummary_other": "Surveillance de {{count}} services · Mises à jour toutes les 10 secondes"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Afficher les métriques d'infrastructure"
}
}
},
"theme": {
"description": "Choisissez le style visuel de votre page de statut publique.",
"label": "Thème",
"options": {
"bold": {
"description": "Grande typographie et espacement assuré. Idéal pour les pages axées sur la marque.",
"name": "Audacieux"
},
"editorial": {
"description": "Titres avec serif et séparateurs fins. Distinctif et calme.",
"name": "Éditorial"
},
"modern": {
"description": "Qualité SaaS avec un point de statut pulsant et des cartes qui s'estompent doucement.",
"name": "Moderne"
},
"refined": {
"description": "Propre, professionnel, proche du look classique de Checkmate.",
"name": "Raffiné"
}
},
"title": "Thème de la page"
},
"themeMode": {
"auto": "Automatique (selon l'appareil du visiteur)",
"dark": "Sombre uniquement",
"label": "Mode de thème",
"light": "Clair uniquement"
}
},
"header": {
"title": "Pages de statut",
"description": "Publiez des pages publiques montrant à vos clients et parties prenantes la disponibilité en temps réel et l'historique des incidents."
},
"editorial": {
"allUp": "Tous les systèmes fonctionnent normalement.",
"reportTitle": "Rapport de service"
},
"footer": {
"poweredBy": "Propulsé par"
}
},
"uptime": {
+116 -18
View File
@@ -73,7 +73,9 @@
"paused": "一時停止中",
"na": "N/A",
"resolved": "解決済み",
"responseTime": "応答時間"
"responseTime": "応答時間",
"idle": "アイドル",
"running": "実行中"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "アクティブ",
"responseTime": "応答時間"
}
},
"statusCode": {
"down": "ダウン",
"networkError": {
"dns": "ホスト名を解決できませんでした(DNSルックアップに失敗)。",
"refused": "サーバーが接続を拒否しました(ポートが閉じているか、サービスがリッスンしていません)。",
"reset": "応答を受信する前に接続がリセットされました。",
"timeout": "サーバーがタイムアウト内に応答しませんでした(リクエストタイムアウト)。",
"tls": "TLSまたはSSLハンドシェイクに失敗しました(証明書または暗号化の問題)。",
"unknown": "ターゲットからのHTTP応答がありませんでした。"
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "インフラストラクチャ",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} 個のモニターを一時停止しました",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "選択したモニターはすでに一時停止されています",
"alreadyPaused_other": "選択したすべてのモニターはすでに一時停止されています",
"alreadyRunning_one": "選択したモニターはすでに実行中です",
"alreadyRunning_other": "選択したすべてのモニターはすでに実行中です"
"alreadyRunning_other": "選択したすべてのモニターはすでに実行中です",
"partialFailure": "{{successCount}} 個のモニターが更新されましたが、{{failedCount}} 個はジョブキューとの同期に失敗しました"
}
}
},
@@ -483,9 +498,9 @@
},
"matchMethod": {
"label": "マッチ方法",
"equal": "",
"include": "",
"regex": ""
"equal": "等しい",
"include": "含む",
"regex": "正規表現"
}
},
"title": "詳細設定"
@@ -497,15 +512,15 @@
"label": "チェック頻度",
"value": {
"fifteenMinutes": "15分",
"fifteenSeconds": "",
"fifteenSeconds": "15秒",
"fiveMinutes": "5分",
"fourMinutes": "",
"oneMinute": "",
"fourMinutes": "4分",
"oneMinute": "1分",
"tenMinutes": "10分",
"thirtyMinutes": "30分",
"thirtySeconds": "",
"thirtySeconds": "30秒",
"threeMinutes": "3分",
"twoMinutes": ""
"twoMinutes": "2分"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "ゲームを選択",
"placeholder": "ゲームを選択"
},
"port": {
"label": "監視するポート",
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Capture エンドポイント",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNSレコードタイプ",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNSサーバー",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "デスクトップ",
"label": "ストラテジー",
"mobile": "モバイル"
}
},
"title": "一般設定",
@@ -564,7 +599,8 @@
"pagespeed": "Webサイトのページ読み込みパフォーマンス、Core Web Vitals、最適化スコアを追跡します。",
"grpc": "gRPCサーバーのホスト名とポートを入力し、必要に応じてヘルスチェック用のサービス名を指定し、表示名を追加してください。",
"websocket": "監視するWebSocket URLを入力し(例: wss://example.com/socket)、表示名を追加してください。",
"hardware": "インフラの CPU、メモリ、ディスク使用率、温度を監視します。Capture エンドポイント URL には完全なパス /api/v1/metrics を含める必要があります (例: http://192.168.1.10:5001/api/v1/metrics)。認証シークレットにはトークン文字列のみを貼り付けてください — 'Bearer' は含めないでください。"
"hardware": "インフラの CPU、メモリ、ディスク使用率、温度を監視します。Capture エンドポイント URL には完全なパス /api/v1/metrics を含める必要があります (例: http://192.168.1.10:5001/api/v1/metrics)。認証シークレットにはトークン文字列のみを貼り付けてください — 'Bearer' は含めないでください。",
"dns": "特定のDNSサーバーが応答しているかを監視します。"
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "HTTP(S)を使用してWebサイトまたはAPIエンドポイントを監視します。",
"optionPingDescription": "ICMP Pingを使用してサーバーがオンラインかどうかを監視します。",
"optionPortDescription": "サーバーの特定のポートが開いているかどうかを監視します。",
"title": "タイプ"
"title": "タイプ",
"optionDnsDescription": "特定のDNSサーバーが応答しているかを監視します。"
},
"thresholds": {
"title": "アラートしきい値",
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "ネットワーク",
"overview": "概要"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "最終完了日時",
"lastRunTook": "最終実行時間",
"lastFailedAt": "最終失敗日時",
"failReason": "失敗理由"
"failReason": "失敗理由",
"state": "状態"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed監視",
"description": "Core Web VitalsとLighthouseスコアを継続的に追跡し、ユーザーが気付く前に劣化を発見します。"
},
"strategy": {
"desktop": "デスクトップ",
"mobile": "モバイル"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "無効な証明書を拒否: 自己署名または信頼されていない証明書の接続を拒否"
},
"displayName": {
"label": "送信者名(任意)- 差出人フィールドに表示されるわかりやすい名前",
"placeholder": "Checkmate通知"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "稼働時間",
"responseTime": "応答時間"
},
"chart": {
"avg": "平均 {{value}}ms",
"downTooltip": "ダウン",
"heatmapAria": "応答時間のヒートマップ",
"max": "最大 {{value}}ms"
},
"status": {
"breached": "しきい値超過",
"down": "ダウン",
"initializing": "初期化中",
"maintenance": "メンテナンス",
"paused": "一時停止中",
"up": "稼働中"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "一部のシステムがメンテナンス中です",
"partiallyPaused": "一部のシステムが一時停止中です",
"initializing": "一部のシステムが初期化中です",
"noMonitors": "モニターが設定されていません"
"noMonitors": "モニターが設定されていません",
"monitoringSummary": "{{count}}サービスを監視中 · 10秒ごとに更新",
"monitoringSummary_other": "{{count}}サービスを監視中 · 10秒ごとに更新"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "インフラストラクチャメトリクスを表示"
}
}
},
"theme": {
"description": "公開ステータスページの視覚スタイルを選択します。",
"label": "テーマ",
"options": {
"bold": {
"description": "大きな書体と自信のある間隔。ブランド中心のページに最適です。",
"name": "ボールド"
},
"editorial": {
"description": "セリフ見出しと細い区切り線。独特で落ち着いています。",
"name": "エディトリアル"
},
"modern": {
"description": "脈動するステータスドットと柔らかくフェードするカードを備えたSaaS品質。",
"name": "モダン"
},
"refined": {
"description": "クリーンでプロフェッショナル、Checkmateのクラシックな外観に近い。",
"name": "リファインド"
}
},
"title": "ページテーマ"
},
"themeMode": {
"auto": "自動(訪問者のデバイスに従う)",
"dark": "ダークのみ",
"label": "テーマモード",
"light": "ライトのみ"
}
},
"header": {
"title": "ステータスページ",
"description": "顧客と関係者にリアルタイムの稼働状況とインシデント履歴を表示する公開ページを公開します。"
},
"editorial": {
"allUp": "すべてのシステムは正常に動作しています。",
"reportTitle": "サービスレポート"
},
"footer": {
"poweredBy": "提供元"
}
},
"uptime": {
+112 -14
View File
@@ -73,7 +73,9 @@
"paused": "pausado",
"na": "N/D",
"resolved": "Resolvido",
"responseTime": "Tempo de resposta"
"responseTime": "Tempo de resposta",
"idle": "Inativo",
"running": "Em execução"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Ativo",
"responseTime": "Tempo de resposta"
}
},
"statusCode": {
"down": "Fora do ar",
"networkError": {
"dns": "Não foi possível resolver o nome do host (a busca DNS falhou).",
"refused": "O servidor recusou a conexão (porta fechada ou serviço não escutando).",
"reset": "A conexão foi redefinida antes que uma resposta fosse recebida.",
"timeout": "O servidor não respondeu a tempo (tempo limite da solicitação esgotado).",
"tls": "Falha no handshake TLS ou SSL (problema de certificado ou criptografia).",
"unknown": "Nenhuma resposta HTTP recebida do destino."
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Infraestrutura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} monitor pausado",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "O monitor selecionado já está pausado",
"alreadyPaused_other": "Todos os monitores selecionados já estão pausados",
"alreadyRunning_one": "O monitor selecionado já está em execução",
"alreadyRunning_other": "Todos os monitores selecionados já estão em execução"
"alreadyRunning_other": "Todos os monitores selecionados já estão em execução",
"partialFailure": "{{successCount}} monitores atualizados, mas {{failedCount}} falharam ao sincronizar com a fila de trabalhos"
}
}
},
@@ -483,8 +498,8 @@
},
"matchMethod": {
"label": "Método de correspondência",
"equal": "",
"include": "",
"equal": "Igual",
"include": "Inclui",
"regex": "Regex"
}
},
@@ -497,13 +512,13 @@
"label": "Frequência de verificação",
"value": {
"fifteenMinutes": "15 minutos",
"fifteenSeconds": "",
"fifteenSeconds": "15 segundos",
"fiveMinutes": "5 minutos",
"fourMinutes": "4 minutos",
"oneMinute": "1 minuto",
"tenMinutes": "10 minutos",
"thirtyMinutes": "30 minutos",
"thirtySeconds": "",
"thirtySeconds": "30 segundos",
"threeMinutes": "3 minutos",
"twoMinutes": "2 minutos"
}
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "Escolher jogo",
"placeholder": "Selecione um jogo"
},
"port": {
"label": "Porta para monitorar",
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Endpoint do Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Tipo de registro DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "Servidor DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Desktop",
"label": "Estratégia",
"mobile": "Mobile"
}
},
"title": "Configurações gerais",
@@ -564,7 +599,8 @@
"pagespeed": "Acompanhe o desempenho de carregamento da página, Core Web Vitals e pontuações de otimização do seu site.",
"grpc": "Digite o hostname e a porta do servidor gRPC, opcionalmente especifique um nome de serviço para a verificação de saúde e adicione um nome de exibição.",
"websocket": "Digite a URL do WebSocket para monitorar (ex.: wss://exemplo.com/socket) e adicione um nome de exibição.",
"hardware": "Monitore o uso de CPU, memória, disco e temperatura da sua infraestrutura. A URL do endpoint do Capture deve incluir o caminho completo /api/v1/metrics (por exemplo, http://192.168.1.10:5001/api/v1/metrics). Para a chave de autorização, cole apenas a string do token — não inclua 'Bearer'."
"hardware": "Monitore o uso de CPU, memória, disco e temperatura da sua infraestrutura. A URL do endpoint do Capture deve incluir o caminho completo /api/v1/metrics (por exemplo, http://192.168.1.10:5001/api/v1/metrics). Para a chave de autorização, cole apenas a string do token — não inclua 'Bearer'.",
"dns": "Monitore se um servidor DNS específico está respondendo."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Use HTTP(S) para monitorar seu site ou endpoint de API.",
"optionPingDescription": "Use ICMP Ping para monitorar se um servidor está online.",
"optionPortDescription": "Monitore se uma porta específica em um servidor está aberta.",
"title": "Tipo"
"title": "Tipo",
"optionDnsDescription": "Monitore se um servidor DNS específico está respondendo."
},
"thresholds": {
"title": "Limites de alerta",
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "Rede",
"overview": "Visão geral"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "Última conclusão em",
"lastRunTook": "Última execução durou",
"lastFailedAt": "Última falha em",
"failReason": "Motivo da falha"
"failReason": "Motivo da falha",
"state": "Estado"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "Monitores PageSpeed",
"description": "Acompanhe Core Web Vitals e pontuações do Lighthouse ao longo do tempo para detectar regressões antes dos seus usuários."
},
"strategy": {
"desktop": "Desktop",
"mobile": "Mobile"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Rejeitar certificados inválidos: Rejeitar conexões com certificados autoassinados ou não confiáveis"
},
"displayName": {
"label": "Nome do remetente (opcional) - Nome amigável exibido no campo De",
"placeholder": "Notificações do Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Uptime",
"responseTime": "Tempo de resposta"
},
"chart": {
"avg": "méd {{value}}ms",
"downTooltip": "Fora do ar",
"heatmapAria": "Mapa de calor do tempo de resposta",
"max": "máx {{value}}ms"
},
"status": {
"breached": "Limite excedido",
"down": "Fora do ar",
"initializing": "Inicializando",
"maintenance": "Manutenção",
"paused": "Pausado",
"up": "Operacional"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Alguns sistemas estão em manutenção",
"partiallyPaused": "Alguns sistemas estão pausados",
"initializing": "Alguns sistemas estão inicializando",
"noMonitors": "Nenhum monitor foi configurado"
"noMonitors": "Nenhum monitor foi configurado",
"monitoringSummary": "Monitorando {{count}} serviço · Atualizações a cada 10 segundos",
"monitoringSummary_other": "Monitorando {{count}} serviços · Atualizações a cada 10 segundos"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Mostrar métricas de infraestrutura"
}
}
},
"theme": {
"description": "Escolha o estilo visual da sua página de status pública.",
"label": "Tema",
"options": {
"bold": {
"description": "Tipografia grande e espaçamento confiante. Ótimo para páginas voltadas à marca.",
"name": "Negrito"
},
"editorial": {
"description": "Títulos com serifa e divisores finos. Distinto e calmo.",
"name": "Editorial"
},
"modern": {
"description": "Qualidade SaaS com um ponto de status pulsante e cartões que desaparecem suavemente.",
"name": "Moderno"
},
"refined": {
"description": "Limpo, profissional, próximo ao visual clássico do Checkmate.",
"name": "Refinado"
}
},
"title": "Tema da página"
},
"themeMode": {
"auto": "Automático (seguir dispositivo do visitante)",
"dark": "Apenas escuro",
"label": "Modo de tema",
"light": "Apenas claro"
}
},
"header": {
"title": "Páginas de status",
"description": "Publique páginas públicas que mostram aos seus clientes e partes interessadas o tempo de atividade em tempo real e o histórico de incidentes."
},
"editorial": {
"allUp": "Todos os sistemas estão operando normalmente.",
"reportTitle": "Relatório de serviço"
},
"footer": {
"poweredBy": "Desenvolvido por"
}
},
"uptime": {
+105 -7
View File
@@ -73,7 +73,9 @@
"paused": "приостановлен",
"na": "Н/Д",
"resolved": "Решён",
"responseTime": "Время ответа"
"responseTime": "Время ответа",
"idle": "Бездействие",
"running": "Выполняется"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Активный",
"responseTime": "Время ответа"
}
},
"statusCode": {
"down": "Не работает",
"networkError": {
"dns": "Не удалось разрешить имя хоста (поиск DNS не удался).",
"refused": "Сервер отказал в соединении (порт закрыт или служба не прослушивает).",
"reset": "Соединение было сброшено до получения ответа.",
"timeout": "Сервер не ответил вовремя (тайм-аут запроса).",
"tls": "Сбой рукопожатия TLS или SSL (проблема с сертификатом или шифрованием).",
"unknown": "HTTP-ответ от цели не получен."
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Инфраструктура",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} монитор приостановлен",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Выбранный монитор уже приостановлен",
"alreadyPaused_other": "Все выбранные мониторы уже приостановлены",
"alreadyRunning_one": "Выбранный монитор уже запущен",
"alreadyRunning_other": "Все выбранные мониторы уже запущены"
"alreadyRunning_other": "Все выбранные мониторы уже запущены",
"partialFailure": "Обновлено {{successCount}} мониторов, но {{failedCount}} не удалось синхронизировать с очередью задач"
}
}
},
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Эндпоинт Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Тип записи DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS-сервер",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Десктоп",
"label": "Стратегия",
"mobile": "Мобильный"
}
},
"title": "Основные настройки",
@@ -564,7 +599,8 @@
"pagespeed": "Отслеживайте производительность загрузки, Core Web Vitals и показатели оптимизации для Вашего сайта.",
"grpc": "Введите имя хоста и порт gRPC-сервера, при необходимости укажите имя сервиса для проверки состояния и добавьте отображаемое имя.",
"websocket": "Введите WebSocket URL для мониторинга (напр., wss://example.com/socket) и добавьте отображаемое имя.",
"hardware": "Отслеживайте использование ЦП, памяти, диска и температуру вашей инфраструктуры. URL эндпоинта Capture должен включать полный путь /api/v1/metrics (например, http://192.168.1.10:5001/api/v1/metrics). В секрете авторизации укажите только строку токена — не включайте 'Bearer'."
"hardware": "Отслеживайте использование ЦП, памяти, диска и температуру вашей инфраструктуры. URL эндпоинта Capture должен включать полный путь /api/v1/metrics (например, http://192.168.1.10:5001/api/v1/metrics). В секрете авторизации укажите только строку токена — не включайте 'Bearer'.",
"dns": "Отслеживайте, отвечает ли конкретный DNS-сервер."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Используйте HTTP(S) для мониторинга Вашего сайта или API-эндпоинта.",
"optionPingDescription": "Используйте ICMP Ping для проверки доступности сервера.",
"optionPortDescription": "Мониторинг доступности определённого порта на сервере.",
"title": "Тип"
"title": "Тип",
"optionDnsDescription": "Отслеживайте, отвечает ли конкретный DNS-сервер."
},
"thresholds": {
"title": "Пороги оповещений",
@@ -909,7 +946,8 @@
"lastFinishedAt": "Последнее завершение",
"lastRunTook": "Последний запуск занял",
"lastFailedAt": "Последняя ошибка",
"failReason": "Причина ошибки"
"failReason": "Причина ошибки",
"state": "Состояние"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "Мониторы PageSpeed",
"description": "Отслеживайте Core Web Vitals и оценки Lighthouse во времени, чтобы заметить регрессии раньше пользователей."
},
"strategy": {
"desktop": "Десктоп",
"mobile": "Мобильный"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Отклонять недействительные сертификаты: отклонять соединения с самоподписанными или ненадёжными сертификатами"
},
"displayName": {
"label": "Имя отправителя (необязательно) - Понятное имя в поле От",
"placeholder": "Уведомления Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Аптайм",
"responseTime": "Время ответа"
},
"chart": {
"avg": "ср {{value}}мс",
"downTooltip": "Не работает",
"heatmapAria": "Тепловая карта времени отклика",
"max": "макс {{value}}мс"
},
"status": {
"breached": "Превышен порог",
"down": "Не работает",
"initializing": "Инициализация",
"maintenance": "Обслуживание",
"paused": "Приостановлено",
"up": "Работает"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Некоторые системы на техобслуживании",
"partiallyPaused": "Некоторые системы приостановлены",
"initializing": "Некоторые системы инициализируются",
"noMonitors": "Мониторы не настроены"
"noMonitors": "Мониторы не настроены",
"monitoringSummary": "Отслеживается {{count}} сервис · Обновление каждые 10 секунд",
"monitoringSummary_other": "Отслеживается {{count}} сервисов · Обновление каждые 10 секунд"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Показывать метрики инфраструктуры"
}
}
},
"theme": {
"description": "Выберите визуальный стиль для вашей публичной страницы состояния.",
"label": "Тема",
"options": {
"bold": {
"description": "Крупная типографика и уверенные отступы. Отлично подходит для брендовых страниц.",
"name": "Жирный"
},
"editorial": {
"description": "Заголовки с засечками и тонкие разделители. Отличительный и спокойный.",
"name": "Редакционный"
},
"modern": {
"description": "Уровень SaaS с пульсирующей точкой состояния и плавно исчезающими карточками.",
"name": "Современный"
},
"refined": {
"description": "Чистый, профессиональный, близкий к классическому виду Checkmate.",
"name": "Утонченный"
}
},
"title": "Тема страницы"
},
"themeMode": {
"auto": "Авто (по устройству посетителя)",
"dark": "Только темная",
"label": "Режим темы",
"light": "Только светлая"
}
},
"header": {
"title": "Страницы состояния",
"description": "Публикуйте публичные страницы со временем работы и историей инцидентов для клиентов и заинтересованных сторон."
},
"editorial": {
"allUp": "Все системы работают в нормальном режиме.",
"reportTitle": "Отчет о сервисе"
},
"footer": {
"poweredBy": "Работает на"
}
},
"uptime": {
+105 -7
View File
@@ -73,7 +73,9 @@
"paused": "หยุดชั่วคราว",
"na": "ไม่มีข้อมูล",
"resolved": "แก้ไขแล้ว",
"responseTime": "เวลาตอบสนอง"
"responseTime": "เวลาตอบสนอง",
"idle": "ไม่ทำงาน",
"running": "กำลังทำงาน"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "ใช้งานอยู่",
"responseTime": "เวลาตอบสนอง"
}
},
"statusCode": {
"down": "ไม่พร้อมใช้งาน",
"networkError": {
"dns": "ไม่สามารถแก้ไขชื่อโฮสต์ได้ (การค้นหา DNS ล้มเหลว)",
"refused": "เซิร์ฟเวอร์ปฏิเสธการเชื่อมต่อ (พอร์ตปิดอยู่หรือบริการไม่ได้รับฟัง)",
"reset": "การเชื่อมต่อถูกรีเซ็ตก่อนที่จะได้รับการตอบกลับ",
"timeout": "เซิร์ฟเวอร์ไม่ตอบกลับทันเวลา (คำขอหมดเวลา)",
"tls": "การจับมือ TLS หรือ SSL ล้มเหลว (ปัญหาใบรับรองหรือการเข้ารหัส)",
"unknown": "ไม่ได้รับการตอบกลับ HTTP จากเป้าหมาย"
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "โครงสร้างพื้นฐาน",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "หยุดจอภาพชั่วคราว {{count}} จอ",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "จอภาพที่เลือกถูกหยุดชั่วคราวแล้ว",
"alreadyPaused_other": "จอภาพที่เลือกทั้งหมดถูกหยุดชั่วคราวแล้ว",
"alreadyRunning_one": "จอภาพที่เลือกกำลังทำงานอยู่แล้ว",
"alreadyRunning_other": "จอภาพที่เลือกทั้งหมดกำลังทำงานอยู่แล้ว"
"alreadyRunning_other": "จอภาพที่เลือกทั้งหมดกำลังทำงานอยู่แล้ว",
"partialFailure": "อัปเดตมอนิเตอร์ {{successCount}} รายการแล้ว แต่ {{failedCount}} รายการไม่สามารถซิงค์กับคิวงานได้"
}
}
},
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "ปลายทาง Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "ประเภทเรกคอร์ด DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "เซิร์ฟเวอร์ DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "เดสก์ท็อป",
"label": "กลยุทธ์",
"mobile": "มือถือ"
}
},
"title": "ตั้งค่าทั่วไป",
@@ -564,7 +599,8 @@
"pagespeed": "ติดตามประสิทธิภาพการโหลดหน้า Core Web Vitals และคะแนนการเพิ่มประสิทธิภาพของเว็บไซต์",
"grpc": "กรอกชื่อโฮสต์และพอร์ตของเซิร์ฟเวอร์ gRPC ระบุชื่อบริการสำหรับ health check (ไม่บังคับ) และตั้งชื่อที่แสดง",
"websocket": "กรอก URL ของ WebSocket ที่ต้องการตรวจสอบ (เช่น wss://example.com/socket) และตั้งชื่อที่แสดง",
"hardware": "ตรวจสอบการใช้ CPU หน่วยความจำ ดิสก์ และอุณหภูมิของโครงสร้างพื้นฐานของคุณ URL ของปลายทาง Capture ต้องรวมเส้นทางแบบเต็ม /api/v1/metrics (ตัวอย่าง: http://192.168.1.10:5001/api/v1/metrics) สำหรับรหัสลับสำหรับยืนยันตัวตน ให้วางเฉพาะสตริงโทเค็น — อย่ารวมคำว่า 'Bearer'"
"hardware": "ตรวจสอบการใช้ CPU หน่วยความจำ ดิสก์ และอุณหภูมิของโครงสร้างพื้นฐานของคุณ URL ของปลายทาง Capture ต้องรวมเส้นทางแบบเต็ม /api/v1/metrics (ตัวอย่าง: http://192.168.1.10:5001/api/v1/metrics) สำหรับรหัสลับสำหรับยืนยันตัวตน ให้วางเฉพาะสตริงโทเค็น — อย่ารวมคำว่า 'Bearer'",
"dns": "ติดตามว่าเซิร์ฟเวอร์ DNS เฉพาะตอบกลับหรือไม่"
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "ใช้ HTTP(S) เพื่อตรวจสอบเว็บไซต์หรือ API endpoint",
"optionPingDescription": "ใช้ ICMP Ping เพื่อตรวจสอบว่าเซิร์ฟเวอร์ออนไลน์อยู่หรือไม่",
"optionPortDescription": "ตรวจสอบว่าพอร์ตเฉพาะบนเซิร์ฟเวอร์เปิดอยู่หรือไม่",
"title": "ประเภท"
"title": "ประเภท",
"optionDnsDescription": "ติดตามว่าเซิร์ฟเวอร์ DNS เฉพาะตอบกลับหรือไม่"
},
"thresholds": {
"title": "เกณฑ์การแจ้งเตือน",
@@ -909,7 +946,8 @@
"lastFinishedAt": "เสร็จสิ้นล่าสุดเมื่อ",
"lastRunTook": "รันล่าสุดใช้เวลา",
"lastFailedAt": "ล้มเหลวล่าสุดเมื่อ",
"failReason": "สาเหตุที่ล้มเหลว"
"failReason": "สาเหตุที่ล้มเหลว",
"state": "สถานะ"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "ตัวตรวจสอบ PageSpeed",
"description": "ติดตาม Core Web Vitals และคะแนน Lighthouse เพื่อจับความถดถอยก่อนที่ผู้ใช้จะสังเกตเห็น"
},
"strategy": {
"desktop": "เดสก์ท็อป",
"mobile": "มือถือ"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "ปฏิเสธใบรับรองที่ไม่ถูกต้อง: ปฏิเสธการเชื่อมต่อด้วยใบรับรอง self-signed หรือไม่น่าเชื่อถือ"
},
"displayName": {
"label": "ชื่อผู้ส่ง (ไม่บังคับ) - ชื่อที่เป็นมิตรซึ่งแสดงในช่องจาก",
"placeholder": "การแจ้งเตือน Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "อัปไทม์",
"responseTime": "เวลาตอบสนอง"
},
"chart": {
"avg": "เฉลี่ย {{value}}ms",
"downTooltip": "ไม่พร้อมใช้งาน",
"heatmapAria": "แผนที่ความร้อนของเวลาตอบสนอง",
"max": "สูงสุด {{value}}ms"
},
"status": {
"breached": "เกินขีดจำกัด",
"down": "ไม่พร้อมใช้งาน",
"initializing": "กำลังเริ่มต้น",
"maintenance": "การบำรุงรักษา",
"paused": "หยุดชั่วคราว",
"up": "ใช้งานได้"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "บางระบบอยู่ระหว่างการบำรุงรักษา",
"partiallyPaused": "บางระบบถูกหยุดชั่วคราว",
"initializing": "บางระบบกำลังเริ่มต้น",
"noMonitors": "ยังไม่มีการกำหนดค่ามอนิเตอร์"
"noMonitors": "ยังไม่มีการกำหนดค่ามอนิเตอร์",
"monitoringSummary": "กำลังตรวจสอบ {{count}} บริการ · อัปเดตทุก 10 วินาที",
"monitoringSummary_other": "กำลังตรวจสอบ {{count}} บริการ · อัปเดตทุก 10 วินาที"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "แสดงเมตริกโครงสร้างพื้นฐาน"
}
}
},
"theme": {
"description": "เลือกรูปแบบภาพสำหรับหน้าสถานะสาธารณะของคุณ",
"label": "ธีม",
"options": {
"bold": {
"description": "ตัวอักษรขนาดใหญ่และการเว้นระยะที่มั่นใจ เหมาะสำหรับหน้าที่เน้นแบรนด์",
"name": "ตัวหนา"
},
"editorial": {
"description": "หัวเรื่องแบบเซริฟและตัวคั่นบาง โดดเด่นและสงบ",
"name": "บรรณาธิการ"
},
"modern": {
"description": "ระดับ SaaS พร้อมจุดสถานะที่เต้นและการ์ดที่จางหายอย่างนุ่มนวล",
"name": "ทันสมัย"
},
"refined": {
"description": "สะอาด เป็นมืออาชีพ ใกล้เคียงกับรูปลักษณ์คลาสสิกของ Checkmate",
"name": "ละเอียดอ่อน"
}
},
"title": "ธีมของหน้า"
},
"themeMode": {
"auto": "อัตโนมัติ (ตามอุปกรณ์ของผู้เข้าชม)",
"dark": "มืดเท่านั้น",
"label": "โหมดธีม",
"light": "สว่างเท่านั้น"
}
},
"header": {
"title": "หน้าสถานะ",
"description": "เผยแพร่หน้าสาธารณะที่แสดง uptime แบบเรียลไทม์และประวัติเหตุการณ์ให้กับลูกค้าและผู้มีส่วนได้ส่วนเสีย"
},
"editorial": {
"allUp": "ทุกระบบทำงานเป็นปกติ",
"reportTitle": "รายงานบริการ"
},
"footer": {
"poweredBy": "ขับเคลื่อนโดย"
}
},
"uptime": {
+123 -25
View File
@@ -73,7 +73,9 @@
"paused": "duraklatıldı",
"na": "N/A",
"resolved": "Çözüldü",
"responseTime": "Yanıt süresi"
"responseTime": "Yanıt süresi",
"idle": "Boşta",
"running": "Çalışıyor"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Aktif",
"responseTime": "Yanıt süresi"
}
},
"statusCode": {
"down": "Çalışmıyor",
"networkError": {
"dns": "Ana bilgisayar adı çözümlenemedi (DNS araması başarısız oldu).",
"refused": "Sunucu bağlantıyı reddetti (bağlantı noktası kapalı veya hizmet dinlemiyor).",
"reset": "Yanıt alınmadan bağlantı sıfırlandı.",
"timeout": "Sunucu zamanında yanıt vermedi (istek zaman aşımına uğradı).",
"tls": "TLS veya SSL el sıkışması başarısız (sertifika veya şifreleme sorunu).",
"unknown": "Hedeften HTTP yanıtı alınamadı."
}
}
},
"components": {
@@ -306,7 +319,7 @@
"common": {
"passwordRules": {
"length": {
"beginning": "",
"beginning": "En az",
"highlighted": "8 karakter uzunluğunda"
},
"lowercase": {
@@ -314,7 +327,7 @@
"highlighted": "Bir küçük harf"
},
"match": {
"beginning": "",
"beginning": "Parolalar",
"highlighted": "eşleşmelidir"
},
"number": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Altyapı",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} monitör duraklatıldı",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Seçili monitör zaten duraklatıldı",
"alreadyPaused_other": "Seçili tüm monitörler zaten duraklatıldı",
"alreadyRunning_one": "Seçili monitör zaten çalışıyor",
"alreadyRunning_other": "Seçili tüm monitörler zaten çalışıyor"
"alreadyRunning_other": "Seçili tüm monitörler zaten çalışıyor",
"partialFailure": "{{successCount}} monitör güncellendi, ancak {{failedCount}} tanesi iş kuyruğuyla senkronize edilemedi"
}
}
},
@@ -483,9 +498,9 @@
},
"matchMethod": {
"label": "Eşleştirme yöntemi",
"equal": "",
"include": "",
"regex": ""
"equal": "Eşit",
"include": "İçerir",
"regex": "Regex"
}
},
"title": "Gelişmiş ayarlar"
@@ -497,15 +512,15 @@
"label": "Kontrol sıklığı",
"value": {
"fifteenMinutes": "15 dakika",
"fifteenSeconds": "",
"fifteenSeconds": "15 saniye",
"fiveMinutes": "5 dakika",
"fourMinutes": "",
"oneMinute": "",
"fourMinutes": "4 dakika",
"oneMinute": "1 dakika",
"tenMinutes": "10 dakika",
"thirtyMinutes": "30 dakika",
"thirtySeconds": "",
"threeMinutes": "",
"twoMinutes": ""
"thirtySeconds": "30 saniye",
"threeMinutes": "3 dakika",
"twoMinutes": "2 dakika"
}
}
},
@@ -534,8 +549,8 @@
"placeholder": "https://www.google.com"
},
"game": {
"label": "",
"placeholder": ""
"label": "Oyun seç",
"placeholder": "Bir oyun seçin"
},
"port": {
"label": "İzlenecek port",
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "Capture uç noktası",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNS kayıt türü",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS sunucusu",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Masaüstü",
"label": "Strateji",
"mobile": "Mobil"
}
},
"title": "Genel ayarlar",
"description": {
"http": "İzlenecek URL veya IP adresini girin (örn. https://example.com/ veya 192.168.1.100) ve panoda görünecek açık bir ad ekleyin.",
"ping": "",
"port": "",
"docker": "",
"ping": "Ping atılacak IP adresini veya ana bilgisayar adını girin (ör. 192.168.1.100 veya example.com) ve panoda görünen net bir görünen ad ekleyin.",
"port": "Sunucunun URL veya IP adresini, port numarasını ve panoda görünen net bir görünen ad girin.",
"docker": "Docker kapsayıcı adını veya kimliğini girin. Kapsayıcı adını (ör. my-app) veya kapsayıcı kimliğini (tam 64 karakterli kimlik veya kısa kimlik) kullanabilirsiniz.",
"game": "Ping atılacak IP adresini veya ana bilgisayar adını ve port numarasını girin (ör. 192.168.1.100 veya example.com) ve oyun türünü seçin. Bazı oyunlar yapılandırma dosyasında sunucu sorgularının etkinleştirilmesini gerektirir — oyun sunucunuzun belgelerine bakın. Protokol ayrıntıları için <gamedigLink>desteklenen oyunlar listesine</gamedigLink> bakın.",
"pagespeed": "Web sitenizin sayfa yükleme performansını, Core Web Vitals metriklerini ve optimizasyon puanlarını takip edin.",
"grpc": "gRPC sunucusunun ana bilgisayar adını ve portunu girin, isteğe bağlı olarak sağlık kontrolü için bir servis adı belirtin ve bir görünen ad ekleyin.",
"websocket": "İzlenecek WebSocket URL'sini girin (örn. wss://example.com/socket) ve bir görünen ad ekleyin.",
"hardware": "Altyapınızın CPU, bellek, disk kullanımını ve sıcaklığını izleyin. Capture uç nokta URL'si tam yolu /api/v1/metrics içermelidir (ör. http://192.168.1.10:5001/api/v1/metrics). Yetkilendirme anahtarına yalnızca token dizesini yapıştırın — 'Bearer' eklemeyin."
"hardware": "Altyapınızın CPU, bellek, disk kullanımını ve sıcaklığını izleyin. Capture uç nokta URL'si tam yolu /api/v1/metrics içermelidir (ör. http://192.168.1.10:5001/api/v1/metrics). Yetkilendirme anahtarına yalnızca token dizesini yapıştırın — 'Bearer' eklemeyin.",
"dns": "Belirli bir DNS sunucusunun yanıt verip vermediğini izleyin."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Web sitenizi veya API uç noktanızı izlemek için HTTP(S) kullanın.",
"optionPingDescription": "Bir sunucunun çevrimiçi olup olmadığını izlemek için ICMP Ping kullanın.",
"optionPortDescription": "Bir sunucuda belirli bir portun açık olup olmadığını izleyin.",
"title": "Tür"
"title": "Tür",
"optionDnsDescription": "Belirli bir DNS sunucusunun yanıt verip vermediğini izleyin."
},
"thresholds": {
"title": "Uyarı eşikleri",
@@ -826,7 +863,7 @@
},
"tabs": {
"labels": {
"network": "",
"network": "Ağ",
"overview": "Genel bakış"
}
},
@@ -909,7 +946,8 @@
"lastFinishedAt": "Son tamamlanma zamanı",
"lastRunTook": "Son çalışma süresi",
"lastFailedAt": "Son başarısızlık zamanı",
"failReason": "Başarısızlık nedeni"
"failReason": "Başarısızlık nedeni",
"state": "Durum"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed izleyicileri",
"description": "Core Web Vitals ve Lighthouse puanlarını zaman içinde izleyerek kullanıcılarınız fark etmeden gerilemeleri yakalayın."
},
"strategy": {
"desktop": "Masaüstü",
"mobile": "Mobil"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "E-posta ayarları",
"description": "Sisteminiz için e-posta ayarlarını yapılandırın. Bu, bildirim ve uyarı göndermek için kullanılır.",
"descriptionTransport": "",
"descriptionTransport": "Bu, NodeMailer için SMTP aktarımı oluşturur",
"descriptionTransportLink": "Özellikleri burada görün",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Geçersiz sertifikaları reddet: Kendinden imzalı veya güvenilmeyen sertifikalı bağlantıları reddet"
},
"displayName": {
"label": "Gönderen adı (isteğe bağlı) - Gönderen alanında gösterilen kullanıcı dostu ad",
"placeholder": "Checkmate bildirimleri"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Çalışma süresi",
"responseTime": "Yanıt süresi"
},
"chart": {
"avg": "ort {{value}}ms",
"downTooltip": "Çalışmıyor",
"heatmapAria": "Yanıt süresi ısı haritası",
"max": "maks {{value}}ms"
},
"status": {
"breached": "Eşik aşıldı",
"down": "Çalışmıyor",
"initializing": "Başlatılıyor",
"maintenance": "Bakım",
"paused": "Duraklatıldı",
"up": "Çalışıyor"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Bazı sistemler bakımda",
"partiallyPaused": "Bazı sistemler duraklatıldı",
"initializing": "Bazı sistemler başlatılıyor",
"noMonitors": "Hiç monitör yapılandırılmadı"
"noMonitors": "Hiç monitör yapılandırılmadı",
"monitoringSummary": "{{count}} hizmet izleniyor · 10 saniyede bir güncellenir",
"monitoringSummary_other": "{{count}} hizmet izleniyor · 10 saniyede bir güncellenir"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Altyapı metriklerini göster"
}
}
},
"theme": {
"description": "Genel durum sayfanız için görsel stili seçin.",
"label": "Tema",
"options": {
"bold": {
"description": "Büyük tipografi ve kendinden emin aralıklar. Marka odaklı sayfalar için harika.",
"name": "Kalın"
},
"editorial": {
"description": "Serifli başlıklar ve ince ayraçlar. Belirgin ve sakin.",
"name": "Editöryal"
},
"modern": {
"description": "Atan bir durum noktası ve yumuşakça solan kartlarla SaaS kalitesinde.",
"name": "Modern"
},
"refined": {
"description": "Temiz, profesyonel, klasik Checkmate görünümüne yakın.",
"name": "Zarif"
}
},
"title": "Sayfa teması"
},
"themeMode": {
"auto": "Otomatik (ziyaretçinin cihazına göre)",
"dark": "Yalnızca koyu",
"label": "Tema modu",
"light": "Yalnızca açık"
}
},
"header": {
"title": "Durum sayfaları",
"description": "Müşterilerinize ve paydaşlarınıza gerçek zamanlı çalışma süresi ve olay geçmişini gösteren herkese açık sayfalar yayınlayın."
},
"editorial": {
"allUp": "Tüm sistemler normal şekilde çalışıyor.",
"reportTitle": "Hizmet raporu"
},
"footer": {
"poweredBy": "Sağlayan"
}
},
"uptime": {
+130 -32
View File
@@ -73,7 +73,9 @@
"paused": "призупинено",
"na": "Н/Д",
"resolved": "Вирішено",
"responseTime": "Час відповіді"
"responseTime": "Час відповіді",
"idle": "Очікування",
"running": "Виконується"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Активний",
"responseTime": "Час відповіді"
}
},
"statusCode": {
"down": "Не працює",
"networkError": {
"dns": "Не вдалося розв'язати ім'я хоста (пошук DNS не вдався).",
"refused": "Сервер відхилив з'єднання (порт закрито або служба не слухає).",
"reset": "З'єднання було скинуто до отримання відповіді.",
"timeout": "Сервер не відповів вчасно (час очікування запиту вичерпано).",
"tls": "Збій рукостискання TLS або SSL (проблема з сертифікатом або шифруванням).",
"unknown": "HTTP-відповідь від цілі не отримана."
}
}
},
"components": {
@@ -306,28 +319,28 @@
"common": {
"passwordRules": {
"length": {
"beginning": "",
"highlighted": ""
"beginning": "Має бути щонайменше",
"highlighted": "8 символів"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "Має містити щонайменше",
"highlighted": "одну малу літеру"
},
"match": {
"beginning": "",
"highlighted": ""
"beginning": "Паролі",
"highlighted": "мають збігатися"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "Має містити щонайменше",
"highlighted": "одну цифру"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "Має містити щонайменше",
"highlighted": "один спеціальний символ (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "Має містити щонайменше",
"highlighted": "одну велику літеру"
}
},
"form": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Інфраструктура",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "{{count}} монітор призупинено",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Вибраний монітор уже призупинено",
"alreadyPaused_other": "Всі вибрані монітори уже призупинено",
"alreadyRunning_one": "Вибраний монітор уже запущено",
"alreadyRunning_other": "Всі вибрані монітори уже запущено"
"alreadyRunning_other": "Всі вибрані монітори уже запущено",
"partialFailure": "Оновлено {{successCount}} моніторів, але {{failedCount}} не вдалося синхронізувати з чергою завдань"
}
}
},
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "Точка доступу Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Тип запису DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS-сервер",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Десктоп",
"label": "Стратегія",
"mobile": "Мобільний"
}
},
"title": "Загальні налаштування",
"description": {
"http": "Введіть URL або IP для моніторингу (напр., https://example.com/ або 192.168.1.100) та додайте зрозуміле ім'я для відображення на панелі.",
"ping": "",
"port": "",
"docker": "",
"ping": "Введіть IP-адресу або ім'я хоста для пінгу (наприклад, 192.168.1.100 або example.com) і додайте чітку відображувану назву, що з'явиться на дашборді.",
"port": "Введіть URL або IP сервера, номер порту та чітку відображувану назву, що з'явиться на дашборді.",
"docker": "Введіть назву або ідентифікатор Docker-контейнера. Можна використовувати назву контейнера (наприклад, my-app) або ідентифікатор (повний 64-символьний або скорочений).",
"game": "Введіть IP-адресу або ім'я хоста та номер порту для пінгу (наприклад, 192.168.1.100 або example.com) і виберіть тип гри. Деяким іграм потрібно ввімкнути серверні запити у файлі конфігурації — перевірте документацію вашого ігрового сервера. Деталі протоколу див. у <gamedigLink>списку підтримуваних ігор</gamedigLink>.",
"pagespeed": "Відстежуйте швидкість завантаження сторінки, показники Core Web Vitals та оцінки оптимізації Вашого сайту.",
"grpc": "Введіть ім'я хоста та порт gRPC-сервера, за бажанням вкажіть назву сервісу для перевірки стану та додайте відображуване ім'я.",
"websocket": "Введіть WebSocket URL для моніторингу (напр., wss://example.com/socket) та додайте відображуване ім'я.",
"hardware": "Відстежуйте використання ЦП, пам'яті, диска та температуру вашої інфраструктури. URL точки доступу Capture має містити повний шлях /api/v1/metrics (наприклад, http://192.168.1.10:5001/api/v1/metrics). У секрет авторизації вставте лише рядок токена — не додавайте 'Bearer'."
"hardware": "Відстежуйте використання ЦП, пам'яті, диска та температуру вашої інфраструктури. URL точки доступу Capture має містити повний шлях /api/v1/metrics (наприклад, http://192.168.1.10:5001/api/v1/metrics). У секрет авторизації вставте лише рядок токена — не додавайте 'Bearer'.",
"dns": "Стежте, чи відповідає певний DNS-сервер."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Використовуйте HTTP(S) для моніторингу Вашого сайту або API-ендпоінту.",
"optionPingDescription": "Використовуйте ICMP Ping для моніторингу доступності сервера.",
"optionPortDescription": "Моніторинг відкритості конкретного порту на сервері.",
"title": "Тип"
"title": "Тип",
"optionDnsDescription": "Стежте, чи відповідає певний DNS-сервер."
},
"thresholds": {
"title": "Порогові значення сповіщень",
@@ -765,7 +802,7 @@
"headers": {
"endTime": "Час завершення",
"resolutionType": "Тип вирішення",
"startTime": "",
"startTime": "Час початку",
"statusCode": "Код статусу"
},
"resolvedIncidents": "Вирішені інциденти",
@@ -809,13 +846,13 @@
}
},
"statBoxes": {
"avgCpuTemperature": "",
"cpuFrequency": "",
"cpuLogical": "",
"cpuPhysical": "",
"avgCpuTemperature": "Середня температура CPU",
"cpuFrequency": "Частота CPU",
"cpuLogical": "CPU (логічний)",
"cpuPhysical": "CPU (фізичний)",
"disk": "Диск",
"memory": "Пам'ять",
"os": ""
"os": "ОС"
},
"table": {
"headers": {
@@ -909,7 +946,8 @@
"lastFinishedAt": "Останнє завершення",
"lastRunTook": "Останній запуск тривав",
"lastFailedAt": "Остання помилка",
"failReason": "Причина помилки"
"failReason": "Причина помилки",
"state": "Стан"
}
},
"queueFallback": {
@@ -929,8 +967,8 @@
},
"table": {
"headers": {
"nextWindow": "",
"repeat": ""
"nextWindow": "Наступне вікно",
"repeat": "Повторити"
}
},
"form": {
@@ -1032,7 +1070,7 @@
},
"telegram": {
"title": "Налаштування Telegram",
"description": "",
"description": "Налаштуйте свого Telegram-бота для надсилання сповіщень.",
"optionBotToken": "Токен бота",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "ID чату",
@@ -1099,6 +1137,10 @@
"header": {
"title": "Монітори PageSpeed",
"description": "Відстежуйте Core Web Vitals і оцінки Lighthouse у часі, щоб помітити регресії раніше за користувачів."
},
"strategy": {
"desktop": "Десктоп",
"mobile": "Мобільний"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "Налаштування пошти",
"description": "Налаштуйте параметри електронної пошти для Вашої системи. Використовується для надсилання сповіщень та попереджень.",
"descriptionTransport": "",
"descriptionTransport": "Це створює SMTP-транспорт для NodeMailer",
"descriptionTransportLink": "Дивіться специфікації тут",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Відхиляти недійсні сертифікати: відхиляти з'єднання із самопідписаними або ненадійними сертифікатами"
},
"displayName": {
"label": "Ім'я відправника (необов'язково) - Дружнє ім'я, що відображається в полі Від",
"placeholder": "Сповіщення Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Час роботи",
"responseTime": "Час відповіді"
},
"chart": {
"avg": "сер {{value}}мс",
"downTooltip": "Не працює",
"heatmapAria": "Теплова карта часу відгуку",
"max": "макс {{value}}мс"
},
"status": {
"breached": "Перевищено поріг",
"down": "Не працює",
"initializing": "Ініціалізація",
"maintenance": "Обслуговування",
"paused": "Призупинено",
"up": "Працює"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Деякі системи на техобслуговуванні",
"partiallyPaused": "Деякі системи призупинено",
"initializing": "Деякі системи ініціалізуються",
"noMonitors": "Не налаштовано жодного монітора"
"noMonitors": "Не налаштовано жодного монітора",
"monitoringSummary": "Відстежується {{count}} сервіс · Оновлення кожні 10 секунд",
"monitoringSummary_other": "Відстежується {{count}} сервісів · Оновлення кожні 10 секунд"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Показувати метрики інфраструктури"
}
}
},
"theme": {
"description": "Виберіть візуальний стиль для вашої публічної сторінки стану.",
"label": "Тема",
"options": {
"bold": {
"description": "Велика типографіка та впевнені відступи. Чудово для брендових сторінок.",
"name": "Жирний"
},
"editorial": {
"description": "Заголовки з засічками та тонкі роздільники. Відмінний і спокійний.",
"name": "Редакційний"
},
"modern": {
"description": "Рівень SaaS з пульсуючою крапкою стану та м'яко зникаючими картками.",
"name": "Сучасний"
},
"refined": {
"description": "Чистий, професійний, близький до класичного вигляду Checkmate.",
"name": "Витончений"
}
},
"title": "Тема сторінки"
},
"themeMode": {
"auto": "Авто (за пристроєм відвідувача)",
"dark": "Тільки темна",
"label": "Режим теми",
"light": "Тільки світла"
}
},
"header": {
"title": "Сторінки статусу",
"description": "Публікуйте публічні сторінки з часом роботи в реальному часі та історією інцидентів для клієнтів і зацікавлених сторін."
},
"editorial": {
"allUp": "Усі системи працюють у нормальному режимі.",
"reportTitle": "Звіт про обслуговування"
},
"footer": {
"poweredBy": "Працює на"
}
},
"uptime": {
+130 -32
View File
@@ -73,7 +73,9 @@
"paused": "tạm dừng",
"na": "N/A",
"resolved": "Đã giải quyết",
"responseTime": "Thời gian phản hồi"
"responseTime": "Thời gian phản hồi",
"idle": "Không hoạt động",
"running": "Đang chạy"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "Đang hoạt động",
"responseTime": "Thời gian phản hồi"
}
},
"statusCode": {
"down": "Ngừng hoạt động",
"networkError": {
"dns": "Không thể phân giải tên máy chủ (tra cứu DNS thất bại).",
"refused": "Máy chủ từ chối kết nối (cổng đã đóng hoặc dịch vụ không lắng nghe).",
"reset": "Kết nối đã được đặt lại trước khi nhận được phản hồi.",
"timeout": "Máy chủ không phản hồi kịp thời (yêu cầu hết thời gian chờ).",
"tls": "Bắt tay TLS hoặc SSL thất bại (vấn đề chứng chỉ hoặc mã hóa).",
"unknown": "Không nhận được phản hồi HTTP từ mục tiêu."
}
}
},
"components": {
@@ -306,28 +319,28 @@
"common": {
"passwordRules": {
"length": {
"beginning": "",
"highlighted": ""
"beginning": "Phải có ít nhất",
"highlighted": "8 ký tự"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "Phải chứa ít nhất",
"highlighted": "một chữ thường"
},
"match": {
"beginning": "",
"highlighted": ""
"beginning": "Mật khẩu",
"highlighted": "phải khớp"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "Phải chứa ít nhất",
"highlighted": "một chữ số"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "Phải chứa ít nhất",
"highlighted": "một ký tự đặc biệt (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "Phải chứa ít nhất",
"highlighted": "một chữ hoa"
}
},
"form": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "Hạ tầng",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "Đã tạm dừng {{count}} màn hình",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "Màn hình đã chọn đã được tạm dừng",
"alreadyPaused_other": "Tất cả các màn hình đã chọn đã được tạm dừng",
"alreadyRunning_one": "Màn hình đã chọn đã đang chạy",
"alreadyRunning_other": "Tất cả các màn hình đã chọn đã đang chạy"
"alreadyRunning_other": "Tất cả các màn hình đã chọn đã đang chạy",
"partialFailure": "Đã cập nhật {{successCount}} trình giám sát, nhưng {{failedCount}} trình giám sát không thể đồng bộ hóa với hàng đợi công việc"
}
}
},
@@ -552,19 +567,40 @@
"captureUrl": {
"label": "Điểm cuối Capture",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "Loại bản ghi DNS",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "Máy chủ DNS",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "Máy tính để bàn",
"label": "Chiến lược",
"mobile": "Di động"
}
},
"title": "Cài đặt chung",
"description": {
"http": "Nhập URL hoặc IP cần giám sát (vd: https://example.com/ hoặc 192.168.1.100) và thêm tên hiển thị rõ ràng trên bảng điều khiển.",
"ping": "",
"port": "",
"docker": "",
"ping": "Nhập địa chỉ IP hoặc tên máy chủ để ping (ví dụ: 192.168.1.100 hoặc example.com) và thêm tên hiển thị rõ ràng xuất hiện trên bảng điều khiển.",
"port": "Nhập URL hoặc IP của máy chủ, số cổng và tên hiển thị rõ ràng xuất hiện trên bảng điều khiển.",
"docker": "Nhập tên hoặc ID của container Docker. Bạn có thể sử dụng tên container (ví dụ: my-app) hoặc ID container (ID đầy đủ 64 ký tự hoặc ID ngắn).",
"game": "Nhập địa chỉ IP hoặc tên máy chủ và số cổng để ping (ví dụ: 192.168.1.100 hoặc example.com) và chọn loại trò chơi. Một số trò chơi yêu cầu bật truy vấn máy chủ trong tệp cấu hình của chúng — hãy xem tài liệu của máy chủ trò chơi. Xem <gamedigLink>danh sách các trò chơi được hỗ trợ</gamedigLink> để biết chi tiết giao thức.",
"pagespeed": "Theo dõi hiệu suất tải trang, Core Web Vitals và điểm tối ưu hóa cho trang web của bạn.",
"grpc": "Nhập tên máy chủ và cổng của máy chủ gRPC, tùy chọn chỉ định tên dịch vụ cho kiểm tra sức khỏe và thêm tên hiển thị.",
"websocket": "Nhập URL WebSocket cần giám sát (vd: wss://example.com/socket) và thêm tên hiển thị.",
"hardware": "Giám sát mức sử dụng CPU, bộ nhớ, ổ đĩa và nhiệt độ của hạ tầng. URL điểm cuối Capture phải bao gồm đường dẫn đầy đủ /api/v1/metrics (ví dụ: http://192.168.1.10:5001/api/v1/metrics). Đối với khóa xác thực, chỉ dán chuỗi token — không bao gồm 'Bearer'."
"hardware": "Giám sát mức sử dụng CPU, bộ nhớ, ổ đĩa và nhiệt độ của hạ tầng. URL điểm cuối Capture phải bao gồm đường dẫn đầy đủ /api/v1/metrics (ví dụ: http://192.168.1.10:5001/api/v1/metrics). Đối với khóa xác thực, chỉ dán chuỗi token — không bao gồm 'Bearer'.",
"dns": "Giám sát xem một máy chủ DNS cụ thể có phản hồi hay không."
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "Sử dụng HTTP(S) để giám sát trang web hoặc điểm cuối API của bạn.",
"optionPingDescription": "Sử dụng ICMP Ping để giám sát xem máy chủ có trực tuyến không.",
"optionPortDescription": "Giám sát xem một cổng cụ thể trên máy chủ có mở không.",
"title": "Loại"
"title": "Loại",
"optionDnsDescription": "Giám sát xem một máy chủ DNS cụ thể có phản hồi hay không."
},
"thresholds": {
"title": "Ngưỡng cảnh báo",
@@ -765,7 +802,7 @@
"headers": {
"endTime": "Thời gian kết thúc",
"resolutionType": "Loại giải quyết",
"startTime": "",
"startTime": "Thời gian bắt đầu",
"statusCode": "Mã trạng thái"
},
"resolvedIncidents": "Sự cố đã giải quyết",
@@ -809,13 +846,13 @@
}
},
"statBoxes": {
"avgCpuTemperature": "",
"cpuFrequency": "",
"cpuLogical": "",
"cpuPhysical": "",
"avgCpuTemperature": "Nhiệt độ CPU trung bình",
"cpuFrequency": "Tần số CPU",
"cpuLogical": "CPU (logic)",
"cpuPhysical": "CPU (vật lý)",
"disk": "Ổ đĩa",
"memory": "Bộ nhớ",
"os": ""
"os": "Hệ điều hành"
},
"table": {
"headers": {
@@ -909,7 +946,8 @@
"lastFinishedAt": "Hoàn thành lần cuối lúc",
"lastRunTook": "Lần chạy cuối mất",
"lastFailedAt": "Lỗi lần cuối lúc",
"failReason": "Lý do lỗi"
"failReason": "Lý do lỗi",
"state": "Trạng thái"
}
},
"queueFallback": {
@@ -929,8 +967,8 @@
},
"table": {
"headers": {
"nextWindow": "",
"repeat": ""
"nextWindow": "Cửa sổ tiếp theo",
"repeat": "Lặp lại"
}
},
"form": {
@@ -1032,7 +1070,7 @@
},
"telegram": {
"title": "Cấu hình Telegram",
"description": "",
"description": "Cấu hình bot Telegram của bạn để gửi thông báo.",
"optionBotToken": "Token bot",
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
"optionChatId": "ID cuộc trò chuyện",
@@ -1099,6 +1137,10 @@
"header": {
"title": "Trình giám sát PageSpeed",
"description": "Theo dõi Core Web Vitals và điểm Lighthouse theo thời gian để phát hiện sự suy giảm trước khi người dùng nhận ra."
},
"strategy": {
"desktop": "Máy tính để bàn",
"mobile": "Di động"
}
},
"settings": {
@@ -1201,7 +1243,7 @@
"email": {
"title": "Cài đặt email",
"description": "Cấu hình cài đặt email cho hệ thống. Được sử dụng để gửi thông báo và cảnh báo.",
"descriptionTransport": "",
"descriptionTransport": "Điều này xây dựng một SMTP transport cho NodeMailer",
"descriptionTransportLink": "Xem thông số kỹ thuật tại đây",
"option": {
"host": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "Từ chối chứng chỉ không hợp lệ: Từ chối kết nối với chứng chỉ tự ký hoặc không đáng tin cậy"
},
"displayName": {
"label": "Tên người gửi (tùy chọn) - Tên thân thiện hiển thị trong trường Từ",
"placeholder": "Thông báo Checkmate"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "Thời gian hoạt động",
"responseTime": "Thời gian phản hồi"
},
"chart": {
"avg": "TB {{value}}ms",
"downTooltip": "Ngừng hoạt động",
"heatmapAria": "Bản đồ nhiệt thời gian phản hồi",
"max": "tối đa {{value}}ms"
},
"status": {
"breached": "Vượt ngưỡng",
"down": "Ngừng hoạt động",
"initializing": "Đang khởi tạo",
"maintenance": "Bảo trì",
"paused": "Tạm dừng",
"up": "Hoạt động"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "Một số hệ thống đang bảo trì",
"partiallyPaused": "Một số hệ thống đã tạm dừng",
"initializing": "Một số hệ thống đang khởi tạo",
"noMonitors": "Chưa cấu hình màn hình giám sát nào"
"noMonitors": "Chưa cấu hình màn hình giám sát nào",
"monitoringSummary": "Đang giám sát {{count}} dịch vụ · Cập nhật mỗi 10 giây",
"monitoringSummary_other": "Đang giám sát {{count}} dịch vụ · Cập nhật mỗi 10 giây"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "Hiển thị số liệu hạ tầng"
}
}
},
"theme": {
"description": "Chọn kiểu hiển thị cho trang trạng thái công khai của bạn.",
"label": "Chủ đề",
"options": {
"bold": {
"description": "Kiểu chữ lớn và khoảng cách tự tin. Tuyệt vời cho các trang tập trung vào thương hiệu.",
"name": "Đậm"
},
"editorial": {
"description": "Tiêu đề serif và dải phân cách mỏng. Đặc biệt và tĩnh lặng.",
"name": "Biên tập"
},
"modern": {
"description": "Chất lượng SaaS với chấm trạng thái nhấp nháy và thẻ mờ dần nhẹ nhàng.",
"name": "Hiện đại"
},
"refined": {
"description": "Sạch sẽ, chuyên nghiệp, gần với giao diện Checkmate cổ điển.",
"name": "Tinh tế"
}
},
"title": "Chủ đề trang"
},
"themeMode": {
"auto": "Tự động (theo thiết bị của khách)",
"dark": "Chỉ tối",
"label": "Chế độ chủ đề",
"light": "Chỉ sáng"
}
},
"header": {
"title": "Trang trạng thái",
"description": "Công bố các trang công khai cho khách hàng và các bên liên quan thấy thời gian hoạt động theo thời gian thực và lịch sử sự cố."
},
"editorial": {
"allUp": "Tất cả các hệ thống đang hoạt động bình thường.",
"reportTitle": "Báo cáo dịch vụ"
},
"footer": {
"poweredBy": "Được cung cấp bởi"
}
},
"uptime": {
+105 -7
View File
@@ -73,7 +73,9 @@
"paused": "已暂停",
"na": "不适用",
"resolved": "已解决",
"responseTime": "响应时间"
"responseTime": "响应时间",
"idle": "空闲",
"running": "运行中"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "活跃",
"responseTime": "响应时间"
}
},
"statusCode": {
"down": "宕机",
"networkError": {
"dns": "无法解析主机名(DNS查找失败)。",
"refused": "服务器拒绝连接(端口关闭或服务未监听)。",
"reset": "在收到响应之前连接被重置。",
"timeout": "服务器未及时响应(请求超时)。",
"tls": "TLS或SSL握手失败(证书或加密问题)。",
"unknown": "未从目标接收HTTP响应。"
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "基础设施",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "已暂停 {{count}} 个监视器",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "所选监视器已暂停",
"alreadyPaused_other": "所有选定的监视器均已暂停",
"alreadyRunning_one": "所选监视器已经在运行",
"alreadyRunning_other": "所有选定的监视器均已在运行"
"alreadyRunning_other": "所有选定的监视器均已在运行",
"partialFailure": "已更新 {{successCount}} 个监控,但有 {{failedCount}} 个无法与任务队列同步"
}
}
},
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Capture 端点",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNS记录类型",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS服务器",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "桌面",
"label": "策略",
"mobile": "移动端"
}
},
"title": "常规设置",
@@ -564,7 +599,8 @@
"pagespeed": "跟踪网站的页面加载性能、Core Web Vitals 和优化评分。",
"grpc": "输入 gRPC 服务器的主机名和端口,可选指定健康检查的服务名称,并添加显示名称。",
"websocket": "输入要监控的 WebSocket URL(例如 wss://example.com/socket),并添加显示名称。",
"hardware": "监控基础设施的 CPU、内存、磁盘使用情况和温度。Capture 端点 URL 必须包含完整路径 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授权密钥只需粘贴令牌字符串——请勿包含 'Bearer'。"
"hardware": "监控基础设施的 CPU、内存、磁盘使用情况和温度。Capture 端点 URL 必须包含完整路径 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授权密钥只需粘贴令牌字符串——请勿包含 'Bearer'。",
"dns": "监控特定DNS服务器是否响应。"
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "使用 HTTP(S) 监控您的网站或 API 端点。",
"optionPingDescription": "使用 ICMP Ping 监控服务器是否在线。",
"optionPortDescription": "监控服务器上的特定端口是否开放。",
"title": "类型"
"title": "类型",
"optionDnsDescription": "监控特定DNS服务器是否响应。"
},
"thresholds": {
"title": "告警阈值",
@@ -909,7 +946,8 @@
"lastFinishedAt": "上次完成于",
"lastRunTook": "上次运行耗时",
"lastFailedAt": "上次失败于",
"failReason": "失败原因"
"failReason": "失败原因",
"state": "状态"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed 监控器",
"description": "持续追踪 Core Web Vitals 和 Lighthouse 评分,在用户察觉之前发现性能回退。"
},
"strategy": {
"desktop": "桌面",
"mobile": "移动端"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "拒绝无效证书:拒绝自签名或不受信任证书的连接"
},
"displayName": {
"label": "发件人姓名(可选)- 显示在发件人字段中的友好名称",
"placeholder": "Checkmate通知"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "正常运行时间",
"responseTime": "响应时间"
},
"chart": {
"avg": "平均 {{value}}ms",
"downTooltip": "宕机",
"heatmapAria": "响应时间热图",
"max": "最大 {{value}}ms"
},
"status": {
"breached": "超出阈值",
"down": "宕机",
"initializing": "初始化中",
"maintenance": "维护中",
"paused": "已暂停",
"up": "运行中"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "部分系统正在维护",
"partiallyPaused": "部分系统已暂停",
"initializing": "部分系统正在初始化",
"noMonitors": "尚未配置任何监控器"
"noMonitors": "尚未配置任何监控器",
"monitoringSummary": "正在监控{{count}}个服务 · 每10秒更新一次",
"monitoringSummary_other": "正在监控{{count}}个服务 · 每10秒更新一次"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "显示基础设施指标"
}
}
},
"theme": {
"description": "为您的公共状态页面选择视觉样式。",
"label": "主题",
"options": {
"bold": {
"description": "大字体和自信的间距。非常适合品牌导向的页面。",
"name": "粗体"
},
"editorial": {
"description": "衬线标题和细分隔线。独特而平静。",
"name": "编辑风格"
},
"modern": {
"description": "SaaS级品质,带有脉动状态点和柔和褪色卡片。",
"name": "现代"
},
"refined": {
"description": "干净、专业,接近经典Checkmate外观。",
"name": "精致"
}
},
"title": "页面主题"
},
"themeMode": {
"auto": "自动(跟随访客设备)",
"dark": "仅深色",
"label": "主题模式",
"light": "仅浅色"
}
},
"header": {
"title": "状态页面",
"description": "发布公开页面,向客户和利益相关者展示实时运行情况和事件历史。"
},
"editorial": {
"allUp": "所有系统运行正常。",
"reportTitle": "服务报告"
},
"footer": {
"poweredBy": "技术支持"
}
},
"uptime": {
+105 -7
View File
@@ -73,7 +73,9 @@
"paused": "已暫停",
"na": "N/A",
"resolved": "已解決",
"responseTime": "回應時間"
"responseTime": "回應時間",
"idle": "閒置",
"running": "執行中"
},
"form": {
"role": {
@@ -120,6 +122,17 @@
"active": "啟用中",
"responseTime": "回應時間"
}
},
"statusCode": {
"down": "停機",
"networkError": {
"dns": "無法解析主機名稱(DNS查詢失敗)。",
"refused": "伺服器拒絕連線(連接埠關閉或服務未監聽)。",
"reset": "在收到回應之前連線被重設。",
"timeout": "伺服器未及時回應(請求逾時)。",
"tls": "TLS或SSL交握失敗(憑證或加密問題)。",
"unknown": "未從目標接收HTTP回應。"
}
}
},
"components": {
@@ -452,7 +465,8 @@
"optionPagespeed": "PageSpeed",
"optionHardware": "基礎架構",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
"optionWebSocket": "WebSocket",
"optionDns": "DNS"
},
"bulkPause": {
"paused_one": "已暫停 {{count}} 個監視器",
@@ -462,7 +476,8 @@
"alreadyPaused_one": "所選監視器已暫停",
"alreadyPaused_other": "所有選定的監視器均已暫停",
"alreadyRunning_one": "所選監視器已經在運行",
"alreadyRunning_other": "所有選定的監視器均已在運行"
"alreadyRunning_other": "所有選定的監視器均已在運行",
"partialFailure": "已更新 {{successCount}} 個監控,但有 {{failedCount}} 個無法與工作佇列同步"
}
}
},
@@ -552,6 +567,26 @@
"captureUrl": {
"label": "Capture 端點",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
},
"dnsRecordType": {
"label": "DNS記錄類型",
"value": {
"A": "A",
"AAAA": "AAAA",
"CNAME": "CNAME",
"MX": "MX",
"NS": "NS",
"TXT": "TXT"
}
},
"dnsServer": {
"label": "DNS伺服器",
"placeholder": "8.8.8.8"
},
"strategy": {
"desktop": "桌面",
"label": "策略",
"mobile": "行動裝置"
}
},
"title": "一般設定",
@@ -564,7 +599,8 @@
"pagespeed": "追蹤您網站的頁面載入效能、Core Web Vitals 和最佳化分數。",
"grpc": "輸入 gRPC 伺服器的主機名稱和連接埠,可選擇指定健康檢查的服務名稱,並新增一個顯示名稱。",
"websocket": "輸入要監控的 WebSocket URL(例如 wss://example.com/socket),並新增一個顯示名稱。",
"hardware": "監控基礎架構的 CPU、記憶體、磁碟使用情況和溫度。Capture 端點 URL 必須包含完整路徑 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授權密鑰僅貼上權杖字串——請勿包含 'Bearer'。"
"hardware": "監控基礎架構的 CPU、記憶體、磁碟使用情況和溫度。Capture 端點 URL 必須包含完整路徑 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授權密鑰僅貼上權杖字串——請勿包含 'Bearer'。",
"dns": "監控特定DNS伺服器是否回應。"
}
},
"ignoreTls": {
@@ -601,7 +637,8 @@
"optionHttpDescription": "使用 HTTP(S) 監控您的網站或 API 端點。",
"optionPingDescription": "使用 ICMP Ping 監控伺服器是否上線。",
"optionPortDescription": "監控伺服器上的特定連接埠是否開啟。",
"title": "類型"
"title": "類型",
"optionDnsDescription": "監控特定DNS伺服器是否回應。"
},
"thresholds": {
"title": "警報閾值",
@@ -909,7 +946,8 @@
"lastFinishedAt": "最後完成於",
"lastRunTook": "最後執行耗時",
"lastFailedAt": "最後失敗於",
"failReason": "失敗原因"
"failReason": "失敗原因",
"state": "狀態"
}
},
"queueFallback": {
@@ -1099,6 +1137,10 @@
"header": {
"title": "PageSpeed 監視器",
"description": "持續追蹤 Core Web Vitals 與 Lighthouse 評分,在使用者察覺前發現效能退化。"
},
"strategy": {
"desktop": "桌面",
"mobile": "行動裝置"
}
},
"settings": {
@@ -1247,6 +1289,10 @@
},
"rejectUnauthorized": {
"label": "拒絕無效憑證:拒絕使用自簽或不受信任憑證的連線"
},
"displayName": {
"label": "寄件者名稱(選填)- 顯示在寄件者欄位的友善名稱",
"placeholder": "Checkmate通知"
}
}
},
@@ -1307,6 +1353,20 @@
"uptime": {
"title": "正常運行時間",
"responseTime": "回應時間"
},
"chart": {
"avg": "平均 {{value}}ms",
"downTooltip": "停機",
"heatmapAria": "回應時間熱圖",
"max": "最大 {{value}}ms"
},
"status": {
"breached": "超過閾值",
"down": "停機",
"initializing": "初始化中",
"maintenance": "維護中",
"paused": "已暫停",
"up": "運行中"
}
},
"statusBar": {
@@ -1324,7 +1384,9 @@
"maintenance": "部分系統正在維護",
"partiallyPaused": "部分系統已暫停",
"initializing": "部分系統正在初始化",
"noMonitors": "尚未設定任何監視器"
"noMonitors": "尚未設定任何監視器",
"monitoringSummary": "正在監控{{count}}個服務 · 每10秒更新一次",
"monitoringSummary_other": "正在監控{{count}}個服務 · 每10秒更新一次"
},
"table": {
"headers": {
@@ -1415,11 +1477,47 @@
"label": "顯示基礎架構指標"
}
}
},
"theme": {
"description": "為您的公開狀態頁面選擇視覺樣式。",
"label": "主題",
"options": {
"bold": {
"description": "大字體和自信的間距。非常適合品牌導向的頁面。",
"name": "粗體"
},
"editorial": {
"description": "襯線標題和細分隔線。獨特而平靜。",
"name": "編輯風格"
},
"modern": {
"description": "SaaS級品質,帶有脈動狀態點和柔和淡出的卡片。",
"name": "現代"
},
"refined": {
"description": "乾淨、專業,接近經典Checkmate外觀。",
"name": "精緻"
}
},
"title": "頁面主題"
},
"themeMode": {
"auto": "自動(依照訪客裝置)",
"dark": "僅深色",
"label": "主題模式",
"light": "僅淺色"
}
},
"header": {
"title": "狀態頁面",
"description": "發佈公開頁面,向客戶和利害關係人顯示即時的運行狀態與事件歷史。"
},
"editorial": {
"allUp": "所有系統運作正常。",
"reportTitle": "服務報告"
},
"footer": {
"poweredBy": "技術支援"
}
},
"uptime": {
+5
View File
@@ -40,6 +40,11 @@ RUN npm run build
COPY --from=frontend-build /app/client/dist ./public
RUN chown -R node:node ./public \
&& chown -R node:node ./dist \
&& chown -R node:node ./scripts
EXPOSE 52345
CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"]
+6 -2
View File
@@ -26,8 +26,12 @@ RUN cp -r src/templates dist/templates
COPY --from=frontend-build /app/client/dist ./public
RUN chmod +x ./scripts/inject-vars.sh
RUN chmod +x ./scripts/inject-vars.sh \
&& chown -R node:node ./public \
&& chown -R node:node ./dist \
&& chown -R node:node ./scripts
EXPOSE 52345
CMD ./scripts/inject-vars.sh && node ./dist/index.js
CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"]
+204
View File
@@ -0,0 +1,204 @@
<div dir="rtl">
<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="README.ar.md">العربية</a> |
🇪🇸 <a href="README.ca.md">Català</a> |
🇨🇿 <a href="README.cs.md">Čeština</a> |
🇩🇪 <a href="README.de.md">Deutsch</a> |
🇪🇸 <a href="README.es.md">Español</a> |
🇫🇮 <a href="README.fi.md">Suomi</a> |
🇫🇷 <a href="README.fr.md">Français</a> |
🇯🇵 <a href="README.ja.md">日本語</a> |
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
🇷🇺 <a href="README.ru.md">Русский</a> |
🇹🇭 <a href="README.th.md">ไทย</a> |
🇹🇷 <a href="README.tr.md">Türkçe</a> |
🇺🇦 <a href="README.uk.md">Українська</a> |
🇻🇳 <a href="README.vi.md">Tiếng Việt</a> |
🇨🇳 <a href="README.zh-CN.md">简体中文</a> |
🇹🇼 <a href="README.zh-TW.md">繁體中文</a>
</p>
> ⚠️ هذه الترجمة مسودة آلية. مرحبًا بمراجعات الناطقين الأصليين عبر pull request.
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>تطبيق مفتوح المصدر لمراقبة الجاهزية والبنية التحتية</strong></p>
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate)
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
<br>
يحتوي هذا المستودع على واجهة المستخدم والخادم الخلفي معًا لـ Checkmate، وهي أداة مراقبة مفتوحة المصدر وقابلة للاستضافة الذاتية، تتيح تتبع عتاد الخوادم، وحالة التشغيل، وأزمنة الاستجابة، والحوادث في الوقت الحقيقي بتصورات أنيقة. يقوم Checkmate بفحص الخادم/الموقع بشكل دوري للتأكد من إمكانية الوصول إليه وعمله على النحو الأمثل، ويوفر تنبيهات وتقارير فورية حول توافر الخدمات المراقبة، وفترات التوقف، وأزمنة الاستجابة.
يمتلك Checkmate أيضًا وكيلًا يُسمى [Capture](https://github.com/bluewave-labs/capture) لجلب البيانات من الخوادم البعيدة. ليس Capture مطلوبًا لتشغيل Checkmate، لكنه يوفر معلومات إضافية حول حالة المعالج، والذاكرة، والقرص، ودرجة الحرارة لخوادمك. يعمل Capture على Linux وWindows وMac وRaspberry Pi، أو أي جهاز قادر على تشغيل Go.
تم اختبار Checkmate تحت الضغط مع أكثر من 1000 مراقبة نشطة دون أي مشاكل أو اختناقات أداء ملحوظة.
## 📚 المحتويات
- [📦 العرض التجريبي](#demo)
- [🔗 دليل المستخدم](#users-guide)
- [🛠️ التثبيت](#installation)
- [🚀 الأداء](#performance)
- [💚 الأسئلة والأفكار](#questions--ideas)
- [🧩 المزايا](#features)
- [🏗️ لقطات الشاشة](#screenshots)
- [🏗️ التقنيات المستخدمة](#tech-stack)
- [🔗 بعض الروابط](#a-few-links)
- [🤝 المساهمة](#contributing)
<a id="demo"></a>
## العرض التجريبي
يمكنك مشاهدة آخر إصدار من [Checkmate](https://demo.checkmate.so/) أثناء التشغيل.
اسم المستخدم هو demouser@demo.com وكلمة المرور Demouser1! (ملاحظة: نقوم بتحديث خادم العرض التجريبي من حين لآخر، فإذا لم يعمل، يُرجى إخبارنا في قناة Discussions).
<a id="users-guide"></a>
## دليل المستخدم
يمكنك العثور على تعليمات الاستخدام [هنا](https://checkmate.so/docs).
## المتطلبات المسبقة
- تثبيت [Docker](https://www.docker.com/)
- تثبيت [Git](https://git-scm.com/)
<a id="installation"></a>
## التثبيت
اطّلع على تعليمات التثبيت في [بوابة وثائق Checkmate](https://checkmate.so/docs).
بدلاً من ذلك، يمكنك استخدام [Coolify](https://coolify.io/)، أو [Elestio](https://elest.io/open-source/checkmate)، أو [K8s](./charts/helm/checkmate/INSTALLATION.md)، أو [Sive Host](https://sive.host) (جنوب أفريقيا)، أو [Cloudzy](https://cloudzy.com/marketplace/checkmate)، أو [Pikapods](https://www.pikapods.com/) لتشغيل نسخة من Checkmate بسرعة. إن أردت مراقبة البنية التحتية لخوادمك، ستحتاج إلى [وكيل Capture](https://github.com/bluewave-labs/capture). يحتوي مستودع Capture أيضًا على تعليمات التثبيت.
### استخدام شهادة CA مخصصة
إذا كنت بحاجة لمراقبة نقاط نهاية HTTPS داخلية بشهادات صادرة عن سلطات شهادات خاصة (مثل Smallstep)، راجع [دليل الثقة بشهادات CA المخصصة](../custom-ca-trust.md) لخيارات إعداد Docker.
للمزيد من الوثائق، راجع [مجلد docs](../).
<a id="performance"></a>
## الأداء
بفضل التحسينات الواسعة، يعمل Checkmate ببصمة ذاكرة صغيرة استثنائيًا، ولا يتطلب سوى الحد الأدنى من الذاكرة وموارد المعالج. فيما يلي استخدام الذاكرة لنسخة Node.js على خادم يراقب 323 خادمًا كل دقيقة:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
ويمكنك أيضًا رؤية بصمة الذاكرة لـ MongoDB وRedis على الخادم نفسه (398 ميجابايت و15 ميجابايت) لنفس عدد الخوادم:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
<a id="questions--ideas"></a>
## الأسئلة والأفكار
إذا كانت لديك أسئلة أو اقتراحات أو تعليقات، فلديك عدة خيارات:
- [قناة Discord](https://discord.gg/NAb6H3UTjK) (مفضّلة)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (نمر عليها من حين لآخر)
لا تتردد في طرح الأسئلة أو مشاركة أفكارك — يسعدنا التواصل معك!
<a id="features"></a>
## المزايا
- مفتوح المصدر بالكامل، يمكن نشره على خوادمك أو على أجهزتك المنزلية (مثل Raspberry Pi 4 أو 5)
- خيارات مراقبة متعددة: الجاهزية، Docker، Ping، SSL، المنفذ، خادم الألعاب
- مراقبة سرعة الصفحة
- مراقبة البنية التحتية (الذاكرة، استخدام القرص، أداء المعالج، الشبكة، إلخ) — يتطلب وكيل [Capture](https://github.com/bluewave-labs/capture)
- مراقبة انتقائية للأقراص مع اختيار نقاط التحميل
- متابعة الحوادث بنظرة واحدة
- صفحات حالة بأربعة قوالب أنيقة
- إشعارات عبر البريد الإلكتروني، Webhooks، Discord، Slack، PagerDuty، Matrix، Microsoft Teams، Telegram، Pushover، Twilio (SMS)
- صيانة مجدولة
- مراقبة باستخدام استعلام JSON
- دعم متعدد اللغات: العربية، الصينية (المبسطة)، الصينية (التقليدية، تايوان)، التشيكية، الإنجليزية، الفنلندية، الفرنسية، الألمانية، اليابانية، البرتغالية (البرازيلية)، الروسية، الإسبانية، التايلاندية، التركية، الأوكرانية، والفيتنامية
## دورة حياة المراقبة
1. تقوم المراقبة بإجراء فحص (HTTP / Ping / منفذ / فحص عتاد عبر وكيل Capture)
2. يتم تخزين النتيجة (نجاح/فشل + زمن الاستجابة)
3. تُقيَّم النتائج الأخيرة وفقًا لعتبة تغيّر الحالة المضبوطة للمراقبة
4. إذا تحققت العتبة وكانت الحالة الحالية تختلف عن السابقة، تتغير حالة المراقبة (مثل `initializing`, `up`, `down`, `breached`)
5. عند تغير الحالة، يتم إنشاء حادث أو حله بحسب الحالة الحالية للمراقبة
6. تُطلَق الإشعارات استنادًا إلى الإعدادات
<a id="screenshots"></a>
## لقطات الشاشة
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
<a id="tech-stack"></a>
## التقنيات المستخدمة
- [ReactJs](https://react.dev/)
- [MUI (إطار عمل React)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- والكثير من المكونات مفتوحة المصدر الأخرى!
<a id="a-few-links"></a>
## بعض الروابط
- إذا أردت دعمنا، فضع ⭐ على المستودع وانقر على "watch".
- لديك سؤال أو اقتراح يخص خارطة الطريق/المزايا؟ تفضل بزيارة [قناة Discord](https://discord.gg/NAb6H3UTjK) أو منتدى [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
- هل تحتاج إشعارًا عند صدور إصدار جديد؟ استخدم [Newreleases](https://newreleases.io/) — وهي خدمة مجانية لتتبع الإصدارات.
- شاهد [فيديو تثبيت Checkmate واستخدامه](https://www.youtube.com/watch?v=GfFOc0xHIwY)
<a id="contributing"></a>
## المساهمة
نحن [Alex](http://github.com/ajhollid) (قائد الفريق)، و[Gorkem](http://github.com/gorkem-bwl/)، و[Aryaman](https://github.com/Br0wnHammer)، و[Mert](https://github.com/mertssmnoglu)، و[Karen](https://github.com/karenvicent) نساعد الأفراد والشركات على مراقبة بنيتهم التحتية وخوادمهم.
نفخر ببناء علاقات قوية مع المساهمين على جميع المستويات. وعلى الرغم من حداثة المشروع، فقد حصل Checkmate بالفعل على أكثر من 7000 نجمة، وجذب أكثر من 90 مساهمًا من جميع أنحاء العالم.
تم تنجيم مستودعنا من قِبل موظفين في **Google، Microsoft، Intel، Cisco، Tencent، Electronic Arts، ByteDance، JP Morgan Chase، Deloitte، Accenture، Foxconn، Broadcom، China Telecom، Barclays، Capgemini، Wipro، Cloudflare، Dassault Systèmes، وNEC** — فلا تتردد، انضم إلينا، وساهم، وتعلّم معنا!
كيف يمكنك المساهمة:
0. ضع نجمة على هذا المستودع :)
1. راجع [دليل المساهمين](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). يُشجَّع المبتدئون على تفقد وسم `good-first-issue`.
2. اقرأ بنية [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) المفصّلة إذا أردت التعمق في المعمارية.
3. افتح issue إذا كنت تعتقد أنك واجهت مشكلة.
4. إذا كنت قادمًا جديدًا، فابحث عن وسوم `good-first-issue`.
5. أنشئ pull request لإضافة ميزات جديدة، أو تحسينات في تجربة المستخدم، أو إصلاح الأخطاء.
6. تصفّح الجولة التفاعلية في قاعدة كود `Checkmate` على CodeCanvas [من هنا](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). لتحسين محاكاة تدفق البيانات الحالية أو إنشاء جديدة، اتبع البرنامج التعليمي السريع [هنا](https://docs.code-canvas.com/updating-diagram).
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date)
</div>
Loaded 100 of 423 files, more files were not shown because too many files have changed in this diff. Show more