Compare commits

..
417 changed files with 5945 additions and 12867 deletions

No files matched your search

+12 -16
View File
@@ -68,23 +68,19 @@ VITE_APP_LOG_LEVEL="debug"
### Backend Layers
```
server/src/
├── api/ # HTTP layer
│ ├── controllers/ # Route handlers (authController, monitorController, etc.)
│ ├── middleware/ # verifyJWT, rateLimiter, sanitization, responseHandler
│ ├── routes/ # API route definitions
│ └── validation/ # Zod request-payload schemas
├── domain/ # Business logic + data access, one folder per entity
│ ├── monitors/ # e.g. monitor.service.ts, monitor.repository.ts, monitor.type.ts
│ ├── checks/
│ ├── incidents/
│ └── … # users, teams, status-pages, notifications, etc.
├── service/ # Cross-cutting services (e.g. infrastructure/)
├── config/ # App config wiring (controllers, routes, services, envValidation)
├── controllers/ # Route handlers (authController, monitorController, etc.)
├── service/ # Business logic
│ ├── business/ # Core monitoring logic
│ ├── infrastructure/ # Server/system utilities
│ └── system/ # App-level settings
├── db/
│ └── migration/ # Database migrations (run on startup)
├── templates/ # Email/notification templates
├── types/ # Shared const tuples + derived types
└── utils/ # Shared utilities
│ ├── 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
```
### Frontend Structure
-1
View File
@@ -151,7 +151,6 @@ LOG_LEVEL="debug"
- `ORIGIN`: Origin for CORS purposes
- `TOKEN_TTL`: Token time to live (in vercel/ms format)
- `LOG_LEVEL`: Debug level (debug, info, warn, error)
- `QUEUE_TYPE`: Job queue implementation (`superSimpleQueue` or `lessSimpleQueue`, optional, default: `superSimpleQueue`)
Start the backend server:
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: checkmate-chart
description: A Helm chart for Checkmate App
type: application
version: 0.2.0
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
version: 0.1.0
appVersion: "2.3"
+12 -142
View File
@@ -19,11 +19,12 @@ cd checkmate/charts/helm/checkmate
### 2. Customize values.yaml
Edit `values.yaml` to update:
- `client.ingress.host` and `api.ingress.host` with your domain names
- `api.protocol` (usually http or https)
- `client.ingress.host` and `server.ingress.host` with your domain names
- `server.protocol` (usually http or https)
- **If upgrading**: Migrate persistence settings from flat structure to nested:
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
- Add: `persistence.mongo.storageClass` (leave empty for default)
- Old: `persistence.redisSize` → New: `persistence.redis.size`
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
@@ -31,7 +32,7 @@ Edit `values.yaml` to update:
```bash
helm install checkmate ./charts/helm/checkmate
```
This will deploy the client, api, and MongoDB components.
This will deploy the client, server, MongoDB, and Redis components.
### 4. Verify the deployment
Check pods and services:
@@ -42,137 +43,6 @@ kubectl get svc
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
## Upgrading from a previous release
This release renames the `server.*` value block to `api.*` and drops the (unused) bundled Redis.
Read this before running `helm upgrade` on an existing install.
### `server.*` → `api.*` (values rename)
The value block formerly named `server` is now `api` (`server.image` → `api.image`,
`server.ingress.host` → `api.ingress.host`, `server.protocol` → `api.protocol`, `server.resources`,
`server.affinity`, `server.tolerations`, `server.ingress.*`, etc.).
**Your existing `server.*` overrides keep working** — the chart merges any legacy `server.*` values on
top of the `api.*` defaults (legacy wins where set), so an unchanged values file upgrades cleanly. The
shim is transitional; migrate your overrides to `api.*` at your convenience and drop the `server:`
block. If you set **both** `server.*` and `api.*` for the same field, the legacy `server.*` value wins —
don't mix them.
### Resource renames cause a replace, not a rename
The Kubernetes objects are renamed alongside the values:
| Old object | New object |
| --- | --- |
| Deployment/Service `checkmate-server` | `checkmate-api` |
| Ingress `checkmate-server-ingress` | `checkmate-api-ingress` |
| ConfigMap `checkmate-server-nginx-cm` | `checkmate-client-nginx-cm` |
Helm deletes the old objects and creates the new ones on upgrade. Expect a brief API restart and a
**new ClusterIP** for the API Service — update anything outside the chart that referenced the
`checkmate-server` Service by name. MongoDB data (its PVC) is untouched.
### Redis removed
The bundled Redis StatefulSet/Service and the `redis.*` / `persistence.redis.*` values are gone — the
scheduler runs in-process and never used them. Default installs (`redis.enabled: false`) are
unaffected. If you had explicitly set `redis.enabled: true`, **the Redis StatefulSet and its PVC are
deleted on upgrade** — back up anything you stored there first. Leftover `redis:` / `persistence.redis:`
keys in your values file are harmless (ignored).
### New secret defaults
`NODE_ENV: production`, `LOG_LEVEL: info`, and `TOKEN_TTL: 99d` are now part of the default `secrets`
block and merge into your install unless you override them. Set them explicitly if you relied on
different values (e.g. a non-production `NODE_ENV`).
### Image tags follow the chart's `appVersion`
Each tier's `image` value is now the repository **without a tag**; the tag defaults to the chart's
`appVersion`, so the client, api, worker, and mongodb images all move together when you upgrade the
chart. You can still pin per tier:
```yaml
api:
image: ghcr.io/bluewave-labs/checkmate-backend # repo only
tag: "" # empty → Chart.appVersion; set e.g. "v3.1.0" to pin, or "latest" for a dev tag
```
- `<tier>.tag` overrides the version for that tier. The `worker` tier inherits `api.tag` (and
`api.image`) unless you set `worker.tag` / `worker.image`.
- If you put a **full `repo:tag`** in `image` (e.g. a legacy `server.image: …:v3.2.0` override), it is
used verbatim and the `tag` field is ignored — so existing pinned values keep working unchanged.
**Prefer a pinned tag over `latest`.** With the default `imagePullPolicy: IfNotPresent`, `latest` is
cached per node and never refreshed, and `helm rollback` can't move a floating tag — pin a version (or
let it default to `appVersion`) for reproducible, rollback-able deploys.
## Scaling: the worker tier & autoscaling
By default the chart runs a single all-in-one API pod that both schedules **and** processes monitoring
jobs (`worker.enabled: false`). For larger deployments you can split processing onto a dedicated,
horizontally-scalable worker tier.
### Topology
| `worker.enabled` | API (`checkmate-api`) | Worker (`checkmate-worker`) |
| --- | --- | --- |
| `false` (default) | schedules **and** processes jobs (`QUEUE_PRIMARY_PROCESSES=true`) | not deployed |
| `true` | schedules + serves the API only (`QUEUE_PRIMARY_PROCESSES=false`, derived) | processes all jobs |
`QUEUE_PRIMARY_PROCESSES` is **derived** from `worker.enabled` — you never set it directly, so the API
and worker tier can never both process (or both ignore) the queue.
### Enabling the worker tier
```yaml
worker:
enabled: true
replicas: 2 # used only when autoscaling.enabled = false
```
The worker takes **no inbound traffic** (no Service): the kubelet probes pod IPs directly on
`HEALTH_PORT` (`/livez`, `/readyz`), and graceful shutdown drains in-flight jobs before exit.
> **Invariant:** `worker.terminationGracePeriodSeconds` (default `30`) **must stay greater than the
> server drain timeout (25s)**. Lower it and Kubernetes SIGKILLs workers mid-drain, losing in-flight
> checks.
> **Caveat:** with the worker tier enabled the API no longer processes jobs, so if **all** workers are
> down nothing processes. The `autoscaling.minReplicaCount: 1` floor plus liveness restarts mitigate
> this; full failover to the API would need leader election (not supported).
### Autoscaling with KEDA
Backlog-driven autoscaling is **off by default** and requires the
[KEDA operator](https://keda.sh/) installed in-cluster. Enable it with:
```yaml
worker:
enabled: true
autoscaling:
enabled: true
minReplicaCount: 1 # processing floor — never 0, or checks silently stall
maxReplicaCount: 10
backlogPerReplica: 50 # target due-job backlog per worker
mongo:
dbName: uptime_db # MUST match the database in DB_CONNECTION_STRING
```
A KEDA `ScaledObject` queries MongoDB directly for the due-check backlog and scales the worker
Deployment between `minReplicaCount` and `maxReplicaCount`. When autoscaling is on, the Deployment
omits `replicas` so it doesn't fight the HPA KEDA creates. Make sure `autoscaling.mongo.dbName`
matches the database name in your `DB_CONNECTION_STRING`, or the scaler counts the wrong DB (always 0
backlog → never scales up).
#### Fallback: Prometheus-adapter HPA (not shipped)
If your KEDA/Mongo versions reject the `$$NOW`/`$toLong` query, or you already run Prometheus, scrape
the worker's `/metrics` (`checkmate_worker_due_backlog`), expose it via prometheus-adapter, and target
it with a standard `HorizontalPodAutoscaler`. The manifest is left to the operator; the app computes
`now` in code on this path, so the query coercion is unnecessary.
## Enabling TLS/HTTPS with cert-manager
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
@@ -198,7 +68,7 @@ client:
enabled: true
secretName: checkmate-client-tls
api:
server:
protocol: https
ingress:
enabled: true
@@ -208,7 +78,7 @@ api:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-api-tls
secretName: checkmate-server-tls
```
### Alternative: Using --set flags
@@ -218,13 +88,13 @@ You can also enable TLS during installation using Helm's `--set` flags:
```bash
helm install checkmate ./charts/helm/checkmate \
--set client.protocol=https \
--set api.protocol=https \
--set server.protocol=https \
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set client.ingress.tls.enabled=true \
--set client.ingress.tls.secretName=checkmate-client-tls \
--set api.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set api.ingress.tls.enabled=true \
--set api.ingress.tls.secretName=checkmate-api-tls
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set server.ingress.tls.enabled=true \
--set server.ingress.tls.secretName=checkmate-server-tls
```
### Verification
@@ -237,7 +107,7 @@ kubectl get certificates
# Check certificate details
kubectl describe certificate checkmate-client-tls
kubectl describe certificate checkmate-api-tls
kubectl describe certificate checkmate-server-tls
# Verify the secrets were created
kubectl get secrets | grep checkmate-tls
@@ -1,35 +0,0 @@
{{- /*
Back-compat shim: the `server.*` value block was renamed to `api.*`. Existing installs that
still pass a legacy `server:` block keep working — this returns the effective API values with any
legacy `server.*` overrides merged on top of the `api.*` defaults (legacy wins where set).
New installs have no `server:` key, so this is a no-op deepCopy of `.Values.api`. Consume it as:
{{- $api := include "checkmate.api" . | fromYaml -}}
then reference `$api.*` instead of `.Values.api.*`.
*/}}
{{- define "checkmate.api" -}}
{{- $api := deepCopy .Values.api -}}
{{- with .Values.server -}}
{{- $api = mergeOverwrite $api (deepCopy .) -}}
{{- end -}}
{{- $api | toYaml -}}
{{- end -}}
{{- /*
Resolve a full image reference from a repo + optional tag. `appVersion` is the single source of
truth for tags: bumping Chart.appVersion moves every tier at once. Call as:
{{ include "checkmate.image" (dict "image" $repo "tag" $tag "root" $) }}
If `image` already carries a tag (a ":" in its final path segment) it is used verbatim — this keeps
legacy `server.image: repo:tag` overrides working and lets anyone pin a full ref (e.g. tag "latest").
A ":" only in the registry segment (a registry port like "reg:5000/x") is ignored, so a tag is still
appended. Otherwise the tag is `tag` if set, else the chart's appVersion.
*/}}
{{- define "checkmate.image" -}}
{{- $lastSegment := .image | splitList "/" | last -}}
{{- if contains ":" $lastSegment -}}
{{- .image -}}
{{- else -}}
{{- printf "%s:%s" .image (.tag | default .root.Chart.AppVersion) -}}
{{- end -}}
{{- end -}}
@@ -1,65 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-api
spec:
replicas: 1 # single scheduler — seeds/reconciles the jobs collection
selector:
matchLabels:
app: checkmate-api
template:
metadata:
labels:
app: checkmate-api
spec:
{{- with $api.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with $api.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: api
image: {{ include "checkmate.image" (dict "image" $api.image "tag" $api.tag "root" $) }}
imagePullPolicy: {{ $api.imagePullPolicy }}
ports:
- name: http
containerPort: {{ $api.port }}
envFrom:
- secretRef:
name: checkmate-secrets
# Probes hit the Express /api/v1/health endpoint — proves the HTTP server is responsive.
# NOTE: this does NOT detect a wedged job loop when queuePrimaryProcesses=true (the API is
# also a worker). Worker-aware health for that mode is a follow-up — see worker-health-checks.md §7.5.
livenessProbe:
httpGet:
path: /api/v1/health
port: http
initialDelaySeconds: {{ $api.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ $api.probes.liveness.periodSeconds }}
timeoutSeconds: {{ $api.probes.liveness.timeoutSeconds }}
failureThreshold: {{ $api.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: /api/v1/health
port: http
initialDelaySeconds: {{ $api.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ $api.probes.readiness.periodSeconds }}
timeoutSeconds: {{ $api.probes.readiness.timeoutSeconds }}
failureThreshold: {{ $api.probes.readiness.failureThreshold }}
{{- with $api.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
env:
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
- name: QUEUE_MODE
value: "primary"
# Derived: when a worker tier is deployed the API only schedules (false); otherwise it is
# all-in-one and processes jobs itself (true). Keeps the two from ever competing for jobs.
- name: QUEUE_PRIMARY_PROCESSES
value: {{ not .Values.worker.enabled | quote }}
@@ -1,10 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: Service
metadata:
name: checkmate-api
spec:
selector:
app: checkmate-api
ports:
- port: {{ $api.port }}
@@ -1,4 +1,3 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -23,13 +22,12 @@ spec:
{{- end }}
containers:
- name: client
image: {{ include "checkmate.image" (dict "image" .Values.client.image "tag" .Values.client.tag "root" $) }}
imagePullPolicy: {{ .Values.client.imagePullPolicy }}
image: {{ .Values.client.image }}
ports:
- containerPort: {{ .Values.client.port }}
env:
- name: UPTIME_APP_API_BASE_URL
value: "{{ $api.protocol }}://{{ $api.ingress.host }}/api/v1"
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
- name: UPTIME_APP_CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
volumeMounts:
@@ -43,4 +41,4 @@ spec:
volumes:
- name: config-volume
configMap:
name: checkmate-client-nginx-cm
name: checkmate-server-nginx-cm
@@ -22,8 +22,7 @@ spec:
{{- end }}
containers:
- name: mongodb
image: {{ include "checkmate.image" (dict "image" .Values.mongodb.image "tag" .Values.mongodb.tag "root" $) }}
imagePullPolicy: {{ .Values.mongodb.imagePullPolicy }}
image: {{ .Values.mongodb.image }}
ports:
- containerPort: {{ .Values.mongodb.port }}
command: ["mongod", "--quiet", "--bind_ip_all"]
@@ -38,9 +37,7 @@ spec:
- metadata:
name: checkmate-mongo-persistent-storage
spec:
{{- with .Values.persistence.mongo.storageClass }}
storageClassName: {{ . | quote }} {{/* omitted when empty so the cluster's default StorageClass applies */}}
{{- end }}
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
accessModes: ["ReadWriteOnce"]
resources:
requests:
+14 -20
View File
@@ -1,15 +1,14 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if eq .Values.client.ingress.host "change_me" }}
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- if eq $api.ingress.host "change_me" }}
{{- fail "api.ingress.host (or legacy server.ingress.host) must be overridden and not set to 'change_me'" }}
{{- if eq .Values.server.ingress.host "change_me" }}
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- $apiProtocol := $api.protocol }}
{{- if not (or (eq $apiProtocol "http") (eq $apiProtocol "https")) }}
{{- fail "api.protocol must be either 'http' or 'https'" }}
{{- $serverProtocol := .Values.server.protocol }}
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
{{- fail "server.protocol must be either 'http' or 'https'" }}
{{- end }}
{{- $clientProtocol := .Values.client.protocol }}
@@ -22,33 +21,28 @@
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
{{- end }}
{{- if and $api.ingress.tls.enabled (ne $apiProtocol "https") }}
{{- fail "api.ingress.tls.enabled is true but api.protocol is not 'https'. Set api.protocol: https to ensure correct API base URL." }}
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
{{- end }}
{{/* If client runs on https, API must also be https to avoid mixed content */}}
{{- if and (eq $clientProtocol "https") (ne $apiProtocol "https") }}
{{- fail "client.protocol is 'https' but api.protocol is not. Set api.protocol: https to prevent browser mixed-content issues." }}
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
{{- end }}
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
{{- $aAnn := $api.ingress.annotations | default dict }}
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
{{- $apiHasIssuer := or (hasKey $aAnn "cert-manager.io/cluster-issuer") (hasKey $aAnn "cert-manager.io/issuer") }}
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{- if and $api.ingress.tls.enabled (not $apiHasIssuer) }}
{{- fail "api.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{/* Worker grace period must exceed the server drain timeout (25s) or workers are SIGKILLed mid-drain */}}
{{- if and .Values.worker.enabled (le (int .Values.worker.terminationGracePeriodSeconds) 25) }}
{{- fail "worker.terminationGracePeriodSeconds must be greater than 25 (the server drain timeout) or workers are killed mid-drain" }}
{{- end }}
{{/* Secret name can be omitted; we default to <release>-client|api-tls in templates */}}
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
@@ -0,0 +1,11 @@
{{- if .Values.redis.enabled }}
apiVersion: v1
kind: Service
metadata:
name: checkmate-redis
spec:
selector:
app: checkmate-redis
ports:
- port: {{ .Values.redis.port }}
{{- end }}
@@ -0,0 +1,33 @@
{{- if .Values.redis.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: checkmate-redis
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-redis
template:
metadata:
labels:
app: checkmate-redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
ports:
- containerPort: {{ .Values.redis.port }}
volumeMounts:
- name: redis-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: checkmate-redis-persistent-storage
spec:
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
{{- end }}
@@ -4,14 +4,6 @@
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
{{- end }}
{{- /*
The bundled Mongo service ("checkmate-mongodb") lives in .Release.Namespace. App pods reach it
by bare name, but the KEDA operator runs in its own namespace and a bare name is NXDOMAIN there,
so worker autoscaling fails to query the backlog. Qualify the in-cluster host to its FQDN here so
the one connection string resolves from every namespace. External/already-qualified hosts are not
matched (the char after the host name is "." for an FQDN, which the pattern excludes).
*/}}
{{- $mongoFqdn := printf "checkmate-mongodb.%s.svc.cluster.local" .Release.Namespace }}
apiVersion: v1
kind: Secret
metadata:
@@ -19,9 +11,5 @@ metadata:
type: Opaque
stringData:
{{- range $key, $value := $secrets }}
{{- if eq $key "DB_CONNECTION_STRING" }}
{{ $key }}: {{ regexReplaceAll "(//|@)checkmate-mongodb([:/,]|$)" $value (printf "${1}%s${2}" $mongoFqdn) | quote }}
{{- else }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-server
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-server
template:
metadata:
labels:
app: checkmate-server
spec:
{{- with .Values.server.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.server.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: server
image: {{ .Values.server.image }}
ports:
- containerPort: {{ .Values.server.port }}
envFrom:
- secretRef:
name: checkmate-secrets
{{- with .Values.server.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
env:
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
@@ -1,12 +1,11 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if $api.ingress.enabled }}
{{- if .Values.server.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkmate-api-ingress
{{- if $api.ingress.annotations }}
name: checkmate-server-ingress
{{- if .Values.server.ingress.annotations }}
annotations:
{{- range $key, $value := $api.ingress.annotations }}
{{- range $key, $value := .Values.server.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
@@ -18,22 +17,22 @@ metadata:
#nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
spec:
ingressClassName: {{ $api.ingress.className }}
{{- if $api.ingress.tls.enabled }}
ingressClassName: {{ .Values.server.ingress.className }}
{{- if .Values.server.ingress.tls.enabled }}
tls:
- hosts:
- {{ $api.ingress.host }}
secretName: {{ default (printf "%s-api-tls" .Release.Name) $api.ingress.tls.secretName }}
- {{ .Values.server.ingress.host }}
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ $api.ingress.host }}
- host: {{ .Values.server.ingress.host }}
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: checkmate-api
name: checkmate-server
port:
number: {{ $api.port }}
number: {{ .Values.server.port }}
{{- end }}
@@ -1,8 +1,7 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
apiVersion: v1
kind: ConfigMap
kind: ConfigMap
metadata:
name: checkmate-client-nginx-cm
name: checkmate-server-nginx-cm
data:
default.conf: |
server {
@@ -23,7 +22,7 @@ data:
}
# location /api/ {
# proxy_pass http://{{ $api.ingress.host }}:5000/api/;
# proxy_pass http://{{ .Values.server.ingress.host }}:5000/api/;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
@@ -32,7 +31,7 @@ data:
# }
location /api-docs/ {
proxy_pass http://{{ $api.ingress.host }}:5000/api-docs/;
proxy_pass http://{{ .Values.server.ingress.host }}:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Service
metadata:
name: checkmate-server
spec:
selector:
app: checkmate-server
ports:
- port: {{ .Values.server.port }}
@@ -1,66 +0,0 @@
{{- $api := include "checkmate.api" . | fromYaml -}}
{{- if .Values.worker.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmate-worker
spec:
{{- if not .Values.worker.autoscaling.enabled }}
replicas: {{ .Values.worker.replicas }} # omitted when autoscaling owns the replica count
{{- end }}
selector:
matchLabels:
app: checkmate-worker
template:
metadata:
labels:
app: checkmate-worker
spec:
terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }}
{{- with .Values.worker.affinity }}
affinity:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
{{- with .Values.worker.tolerations }}
tolerations:
{{- tpl ( . | toYaml) $ | nindent 8 }}
{{- end }}
containers:
- name: worker
image: {{ include "checkmate.image" (dict "image" (.Values.worker.image | default $api.image) "tag" (.Values.worker.tag | default $api.tag) "root" $) }}
imagePullPolicy: {{ .Values.worker.imagePullPolicy | default $api.imagePullPolicy }}
ports:
- name: health
containerPort: {{ .Values.worker.healthPort }}
envFrom:
- secretRef:
name: checkmate-secrets # DB_CONNECTION_STRING (+ JWT_SECRET/CLIENT_HOST for validateEnv)
env:
# Secret default CLIENT_HOST is the non-URL "change_me"; override like the API so validateEnv passes.
- name: CLIENT_HOST
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
- name: QUEUE_MODE
value: "worker"
- name: HEALTH_PORT
value: {{ .Values.worker.healthPort | quote }}
livenessProbe:
httpGet:
path: /livez
port: health
initialDelaySeconds: {{ .Values.worker.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.worker.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.worker.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.worker.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: {{ .Values.worker.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.worker.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.worker.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.worker.probes.readiness.failureThreshold }}
{{- with .Values.worker.resources }}
resources:
{{- tpl ( . | toYaml) $ | nindent 12 }}
{{- end }}
{{- end }}
@@ -1,30 +0,0 @@
{{- if and .Values.worker.enabled .Values.worker.autoscaling.enabled }}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: checkmate-worker
spec:
scaleTargetRef:
name: checkmate-worker
minReplicaCount: {{ .Values.worker.autoscaling.minReplicaCount }} # floor: never 0 — a cold gap stops checks
maxReplicaCount: {{ .Values.worker.autoscaling.maxReplicaCount }}
cooldownPeriod: {{ .Values.worker.autoscaling.cooldownPeriod }}
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: {{ .Values.worker.autoscaling.stabilizationWindowSeconds }}
triggers:
- type: mongodb
metadata:
dbName: {{ .Values.worker.autoscaling.mongo.dbName | quote }}
collection: {{ .Values.worker.autoscaling.mongo.collection | quote }}
# The Phase 2 countDueBacklog predicate, time-relativized with $$NOW (epoch-ms via $toLong):
query: >-
{ "type": "check", "isActive": true,
"$expr": { "$lte": ["$nextScheduledAt", { "$toLong": "$$NOW" }] },
"$or": [ { "lockedUntil": null },
{ "$expr": { "$lt": ["$lockedUntil", { "$toLong": "$$NOW" }] } } ] }
queryValue: {{ .Values.worker.autoscaling.backlogPerReplica | quote }}
connectionStringFromEnv: DB_CONNECTION_STRING # resolved from the worker's checkmate-secrets
{{- end }}
+27 -65
View File
@@ -1,7 +1,5 @@
client:
image: ghcr.io/bluewave-labs/checkmate-client
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0
port: 80
protocol: http
ingress:
@@ -19,24 +17,10 @@ client:
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
api:
image: ghcr.io/bluewave-labs/checkmate-backend
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
server:
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
port: 52345
protocol: http
# Liveness/readiness probes hit GET /api/v1/health on the api port (proves the HTTP server is up).
probes:
liveness:
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readiness:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
ingress:
enabled: true
host: change_me
@@ -47,62 +31,40 @@ api:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-api-tls # Optional; defaults to <release>-api-tls if omitted
# Note: when enabling TLS, also set api.protocol: https and add
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
# Note: when enabling TLS, also set server.protocol: https and add
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
mongodb:
image: ghcr.io/bluewave-labs/checkmate-mongo
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
port: 27017
worker:
# Off by default; existing installs are unchanged. When true, a dedicated worker processes all
# jobs and the API stops processing (QUEUE_PRIMARY_PROCESSES is derived as false). When false, the
# API is all-in-one and processes jobs itself.
redis:
enabled: false
image: "" # defaults to api.image when empty, same as API image
tag: "" # defaults to api.tag (-> Chart.appVersion) when empty
imagePullPolicy: "" # defaults to api.imagePullPolicy when empty
replicas: 1 # used only when autoscaling.enabled = false
healthPort: 52346 # HEALTH_PORT; serves /livez /readyz /metrics
terminationGracePeriodSeconds: 30 # MUST stay > the server drain timeout (25s) or workers are killed mid-drain
resources: {}
affinity: {}
tolerations: {}
probes:
liveness: # /livez — failureThreshold>=3 so one slow tick never restarts mid-job
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readiness: # /readyz — flips to 503 on drain
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
autoscaling:
enabled: false # requires the KEDA operator installed in-cluster
minReplicaCount: 1 # processing floor — never 0 (a cold gap silently stops checks)
maxReplicaCount: 10
backlogPerReplica: 50 # KEDA queryValue: target due-backlog per worker
cooldownPeriod: 300 # seconds before scaling an idle target down
stabilizationWindowSeconds: 300 # damp the interval-boundary herd
mongo:
dbName: uptime_db # must match DB_CONNECTION_STRING's database
collection: jobs
image: redis:7.2
port: 6379
mongodb:
image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0
port: 27017
secrets:
JWT_SECRET: change_me
DB_CONNECTION_STRING: mongodb://checkmate-mongodb:27017/uptime_db
# REFRESH_TOKEN_SECRET: change_me
# SYSTEM_EMAIL_ADDRESS: test@example.com
# SYSTEM_EMAIL_PASSWORD: change_me
# SYSTEM_EMAIL_HOST: smtp.example.com
# SYSTEM_EMAIL_PORT: "587"
# PAGESPEED_API_KEY: change_me
DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db
CLIENT_HOST: change_me
NODE_ENV: production
LOG_LEVEL: info
TOKEN_TTL: 99d
# REDIS_HOST: redis
# REDIS_PORT: "6379"
# DB_TYPE: MongoDB
# TOKEN_TTL: 99d
# REFRESH_TOKEN_TTL: 99d
persistence:
mongo:
size: 5Gi
storageClass: ""
redis:
size: 1Gi
storageClass: ""
+2 -22
View File
@@ -1,14 +1,13 @@
{
"name": "client",
"version": "3.9.1",
"version": "3.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client",
"version": "3.9.1",
"version": "3.7.1",
"dependencies": {
"@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
@@ -304,25 +303,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"license": "MIT",
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "3.9.1",
"version": "3.7.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -14,10 +14,9 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
"**/*": "prettier --write"
},
"dependencies": {
"@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
+1 -1
View File
@@ -22,7 +22,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
width: "100%",
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: props.multiline ? "auto" : 34,
height: 34,
fontSize: typographyLevels.base,
overflow: "hidden",
},
+1 -5
View File
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultHttpMethod, DefaultPageSpeedStrategy } from "@/Types/Monitor";
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions {
@@ -40,13 +40,11 @@ export const getMonitorDefaults = (
...getGeoCheckDefaults(data),
type: "http",
url: data?.url || "",
method: data?.method ?? DefaultHttpMethod,
ignoreTlsErrors: data?.ignoreTlsErrors || false,
useAdvancedMatching: data?.useAdvancedMatching || false,
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
customUpCodes: data?.customUpCodes || [],
};
break;
case "ping":
@@ -135,13 +133,11 @@ export const getMonitorDefaults = (
...getGeoCheckDefaults(data),
type: "http",
url: "",
method: DefaultHttpMethod,
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: "",
expectedValue: "",
jsonPath: "",
customUpCodes: [],
};
}
-8
View File
@@ -78,14 +78,6 @@ 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,7 +31,6 @@ 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",
+21 -143
View File
@@ -1,7 +1,6 @@
import { useMemo, useState } from "react";
import { useEffect } from "react";
import { logger } from "@/Utils/logger";
import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode";
import { useParams, useLocation, useNavigate } from "react-router";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -42,11 +41,8 @@ import {
type Monitor,
type MonitorType,
type GamesMap,
type HttpMethod,
supportsGeoCheck,
DefaultPageSpeedStrategy,
DefaultHttpMethod,
HttpMethods,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { Tag } from "@/Types/Tag";
@@ -262,19 +258,18 @@ const CreateMonitorPage = () => {
resolver: zodResolver(schema),
defaultValues: defaults,
});
const { control, watch, handleSubmit, clearErrors, trigger, reset, setValue } = form;
const { control, watch, handleSubmit, clearErrors, trigger, reset } = form;
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
// Multi-step wizard on every create flow; edit mode keeps the flat form
// where showStep() is always true.
// Multi-step wizard, uptime create only (showTypeSelector). Other flows
// keep the flat form, where showStep() is always true.
const [currentStep, setCurrentStep] = useState(0);
const showStep = (step: number) => isEditMode || currentStep === step;
const showStep = (step: number) => !showTypeSelector || currentStep === step;
const watchedType = watch("type") as MonitorType;
const watchedMethod = watch("method") as HttpMethod | undefined;
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
@@ -367,7 +362,7 @@ const CreateMonitorPage = () => {
refetch={refetchMonitor}
onDelete={handleDeleteClick}
/>
{!isEditMode && (
{showTypeSelector && (
<StepProgress
steps={totalSteps}
current={currentStep}
@@ -1096,144 +1091,27 @@ const CreateMonitorPage = () => {
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="method"
name="useAdvancedMatching"
control={control}
render={({ field }) => (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Select
{...field}
value={field.value ?? DefaultHttpMethod}
onChange={(e) => {
const value = e.target.value as HttpMethod;
field.onChange(value);
// HEAD has no response body, reset advanced matching fields if selected
if (value === "HEAD") {
setValue("useAdvancedMatching", false);
setValue("matchMethod", "");
setValue("expectedValue", "");
setValue("jsonPath", "");
}
}}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.method.label"
)}
>
{HttpMethods.map((method) => (
<MenuItem
key={method}
value={method}
>
{method}
</MenuItem>
))}
</Select>
<Typography
component="span"
color={theme.palette.text.secondary}
sx={{ opacity: 0.8 }}
>
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t(
`pages.createMonitor.form.advanced.option.method.description.${field.value ?? DefaultHttpMethod}`
"pages.createMonitor.form.advanced.option.advancedMatching.label"
)}
</Typography>
</Stack>
)}
/>
<Controller
name="customUpCodes"
control={control}
render={({ field }) => {
const selectedOptions = ALL_HTTP_STATUS_CODES.filter((option) =>
field.value?.includes(option.id)
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
autoHighlight
options={ALL_HTTP_STATUS_CODES}
value={selectedOptions}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, value) => option.id === value.id}
onChange={(
_: unknown,
newValue: typeof ALL_HTTP_STATUS_CODES
) => {
field.onChange(newValue?.map((v) => v.id) || []);
}}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.customUpCodes.label"
)}
/>
<Typography
component="span"
color={theme.palette.text.secondary}
sx={{ opacity: 0.8 }}
>
{t(
"pages.createMonitor.form.advanced.option.customUpCodes.description"
)}
</Typography>
{selectedOptions.length > 0 && (
<Stack
flex={1}
gap={theme.spacing(SPACING.MD)}
width="100%"
>
{selectedOptions.map((option, index) => (
<Stack
direction="row"
alignItems="center"
key={option.id}
width="100%"
>
<Typography flexGrow={1}>{option.name}</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
field.value?.filter((id) => id !== option.id)
);
}}
aria-label={t(
"pages.createMonitor.form.advanced.option.customUpCodes.removeAriaLabel"
)}
>
<Trash2 size={16} />
</IconButton>
{index < selectedOptions.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
{watchedMethod !== "HEAD" && (
<Controller
name="useAdvancedMatching"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t(
"pages.createMonitor.form.advanced.option.advancedMatching.label"
)}
</Typography>
</Stack>
)}
/>
)}
{watchedUseAdvancedMatching && watchedMethod !== "HEAD" && (
{watchedUseAdvancedMatching && (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="matchMethod"
@@ -1457,9 +1335,9 @@ const CreateMonitorPage = () => {
<Stack
direction="row"
justifyContent={!isEditMode ? "space-between" : "flex-end"}
justifyContent={showTypeSelector ? "space-between" : "flex-end"}
>
{!isEditMode && (
{showTypeSelector && (
<Button
type="button"
variant="outlined"
@@ -1470,7 +1348,7 @@ const CreateMonitorPage = () => {
{t("common.buttons.back")}
</Button>
)}
{!isEditMode && currentStep < totalSteps - 1 ? (
{showTypeSelector && currentStep < totalSteps - 1 ? (
<Button
key="wizard-next"
type="button"
+3 -53
View File
@@ -2,9 +2,8 @@ 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";
@@ -16,27 +15,16 @@ 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?page=${page}&rowsPerPage=${rowsPerPage}`,
{},
{ refreshInterval: 1000, keepPreviousData: true }
);
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 1000 });
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;
@@ -58,34 +46,6 @@ 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
@@ -112,17 +72,7 @@ export const TabQueue = () => {
/>
</Typography>
</Stack>
<TableJobs
jobs={jobs}
count={jobsCount}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={(_, newPage) => setPage(newPage)}
onRowsPerPageChange={(event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
}}
/>
<TableJobs jobs={jobs} />
</Stack>
<Stack gap={theme.spacing(3)}>
<Stack gap={theme.spacing(1)}>
@@ -33,26 +33,6 @@ export const MongoStats = ({ diagnostics }: MongoStatsProps) => {
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}
+65 -87
View File
@@ -1,4 +1,4 @@
import { Table, Pagination } from "@/Components/design-elements";
import { Table } from "@/Components/design-elements";
import { Typography, useTheme } from "@mui/material";
import prettyMilliseconds from "pretty-ms";
import { formatTimestamp } from "@/Utils/TimeUtils";
@@ -6,27 +6,14 @@ 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,
count,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
}: TableJobsProps) => {
export const TableJobs = ({ jobs }: TableJobsProps) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -42,15 +29,12 @@ export const TableJobs = ({
maxWidth: 220,
};
const isRunning = (row: QueueJobWithId) =>
row.lockedBy !== null && row.lockedUntil !== null && row.lockedUntil > Date.now();
const headers: Header<QueueJobWithId>[] = [
{
id: "state",
content: t("pages.logs.table.headers.state"),
render: (row) => {
if (isRunning(row)) {
if (row.lockedAt) {
return t("common.labels.running");
}
if (row.monitorActive === true) {
@@ -75,6 +59,18 @@ export const TableJobs = ({
</Typography>
),
},
{
id: "url",
content: t("common.table.headers.url"),
render: (row) => (
<Typography
title={row.monitorUrl ?? ""}
sx={cellSx}
>
{row.monitorUrl}
</Typography>
),
},
{
id: "type",
content: t("common.table.headers.type"),
@@ -83,57 +79,50 @@ export const TableJobs = ({
{
id: "interval",
content: t("common.table.headers.interval"),
render: (row) => (
<Typography sx={cellSx}>{prettyMilliseconds(row.monitorInterval ?? 0)}</Typography>
),
},
{
id: "lastFinishedAt",
content: t("pages.logs.table.headers.lastFinishedAt"),
render: (row) => {
const lastFinishedAt = formatTimestamp(row.lastFinishedAt) ?? "-";
return (
<Typography
title={lastFinishedAt}
sx={cellSx}
>
{lastFinishedAt}
</Typography>
);
},
},
{
id: "nextScheduledAt",
content: t("pages.logs.table.headers.nextScheduledAt"),
render: (row) => {
const nextScheduledAt = formatTimestamp(row.nextScheduledAt) ?? "-";
return (
<Typography
title={nextScheduledAt}
sx={cellSx}
>
{nextScheduledAt}
</Typography>
);
},
},
{
id: "lockedBy",
content: t("pages.logs.table.headers.lockedBy"),
render: (row) => {
const lockedBy = row.lockedBy;
if (!lockedBy) {
return <Typography sx={cellSx}>-</Typography>;
let interval;
if (row.monitorId.toString().includes("geo")) {
interval = row.monitorGeoInterval ?? 0;
} else {
interval = row.repeat ?? 0;
}
const workerId = lockedBy.split(":")[2];
const shortWorkerId = workerId.slice(workerId.length - 8);
return <Typography sx={cellSx}>{prettyMilliseconds(interval)}</Typography>;
},
},
{
id: "lastRun",
content: t("pages.logs.table.headers.lastRunAt"),
render: (row) => {
const v = formatTimestamp(row.lastRunAt) ?? "-";
return (
<Typography
title={workerId}
title={v}
sx={cellSx}
>
{`...${shortWorkerId}`}
{v}
</Typography>
);
},
},
{
id: "lastRunTook",
content: t("pages.logs.table.headers.lastRunTook"),
render: (row) => {
const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-";
return <Typography sx={cellSx}>{value}</Typography>;
},
},
{
id: "lockedAt",
content: t("pages.logs.table.headers.lockedAt"),
render: (row) => {
const v = formatTimestamp(row.lockedAt) ?? "-";
return (
<Typography
title={v}
sx={cellSx}
>
{v}
</Typography>
);
},
@@ -141,29 +130,18 @@ export const TableJobs = ({
];
return (
<>
<Table
headers={headers}
data={jobsWithId}
getRowSx={(row) => ({
...(isRunning(row) && {
"& 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}
/>
</>
<Table
headers={headers}
data={jobsWithId}
getRowSx={(row) => ({
...(row.lockedAt && {
"& td": { backgroundColor: theme.palette.rowStatus.running },
}),
...(row.monitorActive === false && {
"& td": { backgroundColor: theme.palette.rowStatus.paused },
}),
})}
/>
);
};
@@ -1,94 +0,0 @@
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}
/>
);
};
+1 -1
View File
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
const LogsPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<number>(1);
const [activeTab, setActiveTab] = useState<number>(0);
return (
<BasePage headerKey="logs">
<Tabs
@@ -150,8 +150,7 @@ const NotificationsCreatePage = () => {
{watchedType !== "matrix" &&
watchedType !== "telegram" &&
watchedType !== "pushover" &&
watchedType !== "twilio" &&
watchedType !== "ntfy" && (
watchedType !== "twilio" && (
<ConfigBox
title={addressConfig.title}
subtitle={addressConfig.description}
@@ -175,48 +174,6 @@ 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")}
+299 -412
View File
@@ -1,4 +1,4 @@
import { BasePage, ConfigBox, StepProgress } from "@/Components/design-elements";
import { BasePage, ConfigBox } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import { logger } from "@/Utils/logger";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
@@ -26,7 +26,6 @@ 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";
@@ -87,27 +86,13 @@ const CreateStatusPage = () => {
defaultValues: defaults,
});
const { control, reset, handleSubmit, trigger } = form;
const { control, reset, handleSubmit } = 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 ?? [])
@@ -151,18 +136,12 @@ 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));
fd.append("showUptimePercentage", String(data.showUptimePercentage));
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
fd.append("showInfrastructure", String(data.showInfrastructure));
fd.append("customCSS", data.customCSS ?? "");
if (data.theme) fd.append("theme", data.theme);
if (data.themeMode) fd.append("themeMode", data.themeMode);
@@ -221,382 +200,315 @@ const CreateStatusPage = () => {
onSubmit={handleSubmit(onSubmit, onError)}
>
{!isCreate && <HeaderConfigStatusControls onDelete={handleDeleteClick} />}
{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)}
/>
)}
/>
<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="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}
/>
)}
/>
<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={
<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="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>
);
}}
/>
}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.statusPages.form.timezone.title")}
subtitle={t("pages.statusPages.form.timezone.description")}
rightContent={
<Controller
name="timezone"
name="isPublished"
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"
)}
/>
)}
<SwitchComponent
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
)}
/>
}
/>
)}
{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={
<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="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.customCSS.title")}
subtitle={t("pages.statusPages.form.customCSS.description")}
rightContent={
<Controller
name="customCSS"
name="companyName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
multiline
rows={8}
fieldLabel={t(
"pages.statusPages.form.customCSS.option.customCSS.label"
)}
fieldLabel={t("pages.statusPages.form.basicInfo.option.name.label")}
placeholder={t(
"pages.statusPages.form.customCSS.option.customCSS.placeholder"
"pages.statusPages.form.basicInfo.option.name.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
}
/>
)}
{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}
<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}
/>
}
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"
{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"
)}
/>
)}
/>
{/* <Controller
)}
/>
}
/>
<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>
}
/>
<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}
/>
}
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 }) => (
@@ -630,46 +542,21 @@ const CreateStatusPage = () => {
/>
)}
/> */}
</Stack>
}
/>
)}
</Stack>
}
/>
<Stack
direction="row"
justifyContent={isCreate ? "space-between" : "flex-end"}
justifyContent="flex-end"
>
{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>
)}
<Button
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
</Stack>
<Dialog
open={isDeleteDialogOpen}
@@ -8,8 +8,7 @@ import { Settings, ExternalLink } from "lucide-react";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { type StatusPage } from "@/Types/StatusPage";
import { getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
interface HeaderStatusPageControlsProps {
isAdmin: boolean;
@@ -51,7 +50,7 @@ export const HeaderStatusPageControls = ({
<Typography
onClick={() => {
window.open(
getStatusPagePublicUrl(statusPage),
`${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`,
"_blank",
"noopener,noreferrer"
);
+14 -38
View File
@@ -2,7 +2,6 @@ import { BasePage, BaseFallback } from "@/Components/design-elements";
import Typography from "@mui/material/Typography";
import { Link } from "react-router-dom";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
@@ -15,12 +14,6 @@ import {
type StatusPageResponse,
type StatusPageTheme,
} from "@/Types/StatusPage";
import {
buildStatusPageApiPath,
getStatusPagePreviewUrl,
getStatusPagePublicUrl,
isCustomDomainHost,
} from "@/Utils/statusPageUrl";
import { HeaderStatusPageControls } from "@/Pages/StatusPage/Status/Components/HeaderStatusPageControls";
import { StatusPageThemeProvider } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
import {
@@ -40,7 +33,6 @@ import { BoldHero } from "@/Pages/StatusPage/Status/themes/bold/BoldHero";
import { editorialStyles } from "@/Pages/StatusPage/Status/themes/editorial/styles";
import { EditorialHeader } from "@/Pages/StatusPage/Status/themes/editorial/EditorialHeader";
import { EditorialHero } from "@/Pages/StatusPage/Status/themes/editorial/EditorialHero";
import { minimalStyles } from "@/Pages/StatusPage/Status/themes/minimal/styles";
const THEME_CONFIGS: Record<StatusPageTheme, ThemeConfig<any>> = {
refined: {
@@ -66,11 +58,6 @@ const THEME_CONFIGS: Record<StatusPageTheme, ThemeConfig<any>> = {
HeroSlot: EditorialHero,
overallStatusOptions: { allUpKey: "pages.statusPages.editorial.allUp" },
},
minimal: {
createStyles: minimalStyles,
HeaderSlot: RefinedHeader,
HeroSlot: RefinedHero,
},
};
const StatusPageView = () => {
@@ -80,14 +67,9 @@ const StatusPageView = () => {
const isAdmin = useIsAdmin();
const location = useLocation();
const onCustomDomainHost = isCustomDomainHost();
const isPublic =
onCustomDomainHost || location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
const apiUrl = buildStatusPageApiPath({
url,
useCustomDomain: onCustomDomainHost,
});
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const { data, isLoading, error } = useGet<StatusPageResponse>(
apiUrl,
@@ -129,9 +111,17 @@ const StatusPageView = () => {
);
}
const themeConfig = THEME_CONFIGS[resolveStatusPageTheme(statusPage.theme)];
const themedRenderer = (
<BaseStatusPage
statusPage={statusPage}
monitors={monitors}
config={themeConfig}
/>
);
// Public route: render directly on the viewport, themed background covers everything.
if (isPublic) {
const themeConfig = THEME_CONFIGS[resolveStatusPageTheme(statusPage.theme)];
return (
<StatusPageThemeProvider
theme={statusPage.theme}
@@ -139,17 +129,12 @@ const StatusPageView = () => {
timezone={statusPage.timezone}
paintBody
>
<BaseStatusPage
statusPage={statusPage}
monitors={monitors}
config={themeConfig}
/>
{themedRenderer}
</StatusPageThemeProvider>
);
}
const publicUrl = getStatusPagePublicUrl(statusPage);
const previewUrl = getStatusPagePreviewUrl(statusPage);
const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
return (
<BasePage
loading={isLoading}
@@ -168,16 +153,7 @@ const StatusPageView = () => {
timezone={statusPage.timezone}
transparent
>
<BrowserFrame url={publicUrl}>
<Box
component="iframe"
src={previewUrl}
title={t("pages.statusPages.preview.title")}
flex={1}
width="100%"
border={0}
/>
</BrowserFrame>
<BrowserFrame url={publicUrl}>{themedRenderer}</BrowserFrame>
</StatusPageThemeProvider>
</BasePage>
);
@@ -64,13 +64,7 @@ export const BrowserFrame = ({ url, children }: Props) => {
{url}
</Box>
</Box>
<Box
display="flex"
flexDirection="column"
sx={{ flex: 1, minHeight: 0 }}
>
{children}
</Box>
<Box sx={{ flex: 1, minHeight: 0 }}>{children}</Box>
</Box>
);
};
@@ -99,9 +99,9 @@ export const boldStyles = (
},
top: {
alignItems: { xs: "flex-start", md: "center" },
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
mb: "36px",
},
brand: {
@@ -130,7 +130,7 @@ export const boldStyles = (
hero: {
position: "relative",
borderRadius: "22px",
padding: { xs: "24px 20px", md: "36px 36px 32px" },
padding: "36px 36px 32px",
mb: "28px",
background: isDark ? heroBgDark : heroBgLight,
border: `1px solid ${tokens.border}`,
@@ -149,14 +149,13 @@ export const boldStyles = (
},
heroTitle: {
m: 0,
fontSize: { xs: 24, md: 34 },
fontSize: 34,
fontWeight: 800,
letterSpacing: "-0.02em",
lineHeight: 1.1,
color: tokens.text,
display: "flex",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
alignItems: "center",
gap: "10px",
},
heroCheck: (tone) => ({
@@ -172,15 +171,9 @@ export const boldStyles = (
}),
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
chartSwitchWrap: {
display: "flex",
justifyContent: { xs: "center", md: "flex-end" },
mb: "16px",
},
chartSwitchWrap: { display: "flex", justifyContent: "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,
@@ -188,20 +181,16 @@ 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: { xs: "8px 6px", md: "8px 18px" },
padding: "8px 18px",
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: { xs: "0.04em", md: "0.12em" },
letterSpacing: "0.12em",
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
@@ -228,21 +217,13 @@ export const boldStyles = (
},
cardRow: {
display: "grid",
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" },
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "24px",
p: "22px 28px",
},
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
cardLeft: { minWidth: 0 },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 700,
fontSize: 17,
letterSpacing: "-0.01em",
@@ -252,9 +233,10 @@ export const boldStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
gridArea: "meta",
display: "flex",
gap: "10px",
alignItems: { xs: "flex-start", md: "center" },
alignItems: "center",
mt: "6px",
flexWrap: "wrap",
},
pill: {
@@ -291,8 +273,6 @@ export const boldStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 800,
padding: "6px 14px",
@@ -316,10 +296,10 @@ export const boldStyles = (
}),
heatmap: {
padding: { xs: "0 12px 18px", md: "0 28px 22px" },
padding: "0 28px 22px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
height: 48,
},
heatmapCell: (kind) => ({
@@ -332,10 +312,10 @@ export const boldStyles = (
}),
histogram: {
padding: { xs: "0 12px", md: "0 28px" },
padding: "0 28px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
alignItems: "flex-end",
height: 48,
},
@@ -90,9 +90,9 @@ export const editorialStyles = (
},
top: {
alignItems: { xs: "flex-start", md: "flex-end" },
display: "flex",
alignItems: "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: { xs: 18, md: 22 },
fontSize: 22,
fontWeight: 400,
m: 0,
mb: "8px",
@@ -144,32 +144,26 @@ export const editorialStyles = (
letterSpacing: "0.12em",
color: tokens.textMuted,
m: 0,
mb: { xs: "32px", md: "56px" },
mb: "56px",
},
chartSwitchWrap: {
display: "flex",
justifyContent: { xs: "center", md: "flex-end" },
justifyContent: "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: { xs: "0.05em", md: "0.2em" },
padding: { xs: "6px 6px", md: "6px 14px" },
letterSpacing: "0.2em",
padding: "6px 14px",
border: 0,
background: active ? tokens.text : "transparent",
color: active ? tokens.bg : tokens.text,
@@ -194,20 +188,12 @@ export const editorialStyles = (
},
cardRow: {
display: "grid",
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" },
gridTemplateColumns: "1fr auto",
alignItems: "baseline",
gap: "20px",
},
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
cardLeft: { minWidth: 0 },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontSize: 20,
fontWeight: 700,
letterSpacing: "-0.015em",
@@ -217,13 +203,14 @@ 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: { xs: "flex-start", md: "center" },
alignItems: "center",
},
pill: {
textTransform: "uppercase",
@@ -247,8 +234,6 @@ export const editorialStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
fontSize: 10,
fontWeight: 700,
@@ -265,7 +250,7 @@ export const editorialStyles = (
mt: "16px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "2px" },
gap: "2px",
height: 40,
},
heatmapCell: (kind) => ({
@@ -277,7 +262,7 @@ export const editorialStyles = (
mt: "16px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "2px" },
gap: "2px",
alignItems: "flex-end",
height: 40,
},
@@ -1,66 +0,0 @@
import type { SxProps, Theme } from "@mui/material/styles";
import type { StatusPageThemeTokens } from "../tokens";
import type { OverallTone } from "../shared/overallStatus";
import { refinedStyles, type RefinedStyles } from "../refined/styles";
type FlatSx = Exclude<SxProps<Theme>, readonly unknown[] | ((theme: Theme) => unknown)>;
const extend = (base: SxProps<Theme>, overrides: SxProps<Theme>): SxProps<Theme> => [
base as FlatSx,
overrides as FlatSx,
];
export const minimalStyles = (
tokens: StatusPageThemeTokens,
isDark: boolean
): RefinedStyles => {
const base = refinedStyles(tokens, isDark);
return {
...base,
page: extend(base.page, {
maxWidth: 820,
p: { xs: "32px 16px 64px", md: "48px 20px 72px" },
}),
top: extend(base.top, {
mb: "44px",
pb: "16px",
borderBottom: `1px solid ${tokens.border}`,
}),
logoMono: extend(base.logoMono, {
borderRadius: "50%",
background: tokens.text,
}),
hero: extend(base.hero, {
alignItems: "center",
background: "transparent",
border: 0,
boxShadow: "none",
p: 0,
mb: "40px",
}),
heroRow: extend(base.heroRow, { justifyContent: "center" }),
heroTitle: extend(base.heroTitle, {
flex: "initial",
fontSize: { xs: 22, md: 28 },
fontWeight: 650,
textAlign: "center",
}),
heroIcon: (_tone: OverallTone) => ({ display: "none" }),
heroSub: extend(base.heroSub, { textAlign: "center", mt: "4px" }),
chartSwitch: extend(base.chartSwitch, { borderRadius: tokens.radius }),
card: extend(base.card, {
boxShadow: "none",
transition: "none",
"&:hover": { transform: "none", boxShadow: "none" },
}),
cardRow: extend(base.cardRow, { p: "14px 16px" }),
badge: (tone) =>
extend(base.badge(tone), {
background: "transparent",
p: 0,
}),
gauge: extend(base.gauge, { borderRadius: tokens.radius }),
footer: extend(base.footer, { mt: "48px", fontSize: 11 }),
};
};
@@ -11,24 +11,24 @@ export const ModernHero = ({
const { t } = useTranslation();
return (
<Box sx={styles.hero}>
<Box sx={styles.heroRow}>
<Box sx={styles.pulse(overall.tone)} />
<Box sx={styles.pulse(overall.tone)} />
<Box sx={styles.statusCopy}>
<Box
component="h1"
sx={styles.heroTitle}
>
{overall.message}
</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
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
</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>;
heroRow: SxProps<Theme>;
statusCopy: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroSub: SxProps<Theme>;
heroIcon: (tone: OverallTone) => SxProps<Theme>;
@@ -117,9 +117,9 @@ export const modernStyles = (
},
top: {
alignItems: { xs: "flex-start", md: "center" },
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
mb: "32px",
},
brand: {
@@ -148,11 +148,11 @@ export const modernStyles = (
hero: {
position: "relative",
borderRadius: "20px",
padding: { xs: "20px", md: "28px 32px" },
padding: "28px 32px",
mb: "24px",
display: "flex",
flexDirection: "column",
gap: "4px",
alignItems: "center",
gap: "20px",
background: tokens.surface,
border: `1px solid ${tokens.border}`,
boxShadow: cardShadow,
@@ -182,37 +182,25 @@ export const modernStyles = (
},
};
},
heroRow: {
display: "flex",
alignItems: "center",
gap: { xs: "14px", md: "20px" },
},
statusCopy: { flex: 1, minWidth: 0 },
heroTitle: {
m: 0,
flex: 1,
minWidth: 0,
fontSize: { xs: 18, md: 22 },
mb: "4px",
fontSize: 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: { xs: "center", md: "flex-end" },
mb: "14px",
},
chartSwitchWrap: { display: "flex", justifyContent: "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,
@@ -220,20 +208,16 @@ 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: { xs: "6px 6px", md: "6px 16px" },
padding: "6px 16px",
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: { xs: "0.04em", md: "0.08em" },
letterSpacing: "0.08em",
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
@@ -272,21 +256,13 @@ export const modernStyles = (
},
cardRow: {
display: "grid",
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" },
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "20px",
p: "18px 24px",
},
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
cardLeft: { minWidth: 0 },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 600,
fontSize: 15,
letterSpacing: "-0.005em",
@@ -296,9 +272,10 @@ export const modernStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
gridArea: "meta",
display: "flex",
gap: "10px",
alignItems: { xs: "flex-start", md: "center" },
alignItems: "center",
mt: "6px",
flexWrap: "wrap",
},
pill: {
@@ -322,8 +299,6 @@ export const modernStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 700,
padding: "5px 12px",
@@ -344,10 +319,10 @@ export const modernStyles = (
}),
heatmap: {
padding: { xs: "0 12px 16px", md: "0 24px 20px" },
padding: "0 24px 20px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
height: 46,
},
heatmapCell: (kind) => ({
@@ -359,10 +334,10 @@ export const modernStyles = (
}),
histogram: {
padding: { xs: "0 12px", md: "0 24px" },
padding: "0 24px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
alignItems: "flex-end",
height: 46,
},
@@ -11,24 +11,24 @@ export const RefinedHero = ({
const { t } = useTranslation();
return (
<Box sx={styles.hero}>
<Box sx={styles.heroRow}>
<Box sx={styles.statusDot(overall.tone)} />
<Box sx={styles.statusDot(overall.tone)} />
<Box sx={styles.statusCopy}>
<Box
component="h1"
sx={styles.heroTitle}
>
{overall.message}
</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
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitorCount,
})}
</Box>
</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>;
heroRow: SxProps<Theme>;
statusCopy: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroSub: SxProps<Theme>;
heroIcon: (tone: OverallTone) => SxProps<Theme>;
@@ -102,9 +102,9 @@ export const refinedStyles = (
},
top: {
alignItems: { xs: "flex-start", md: "center" },
display: "flex",
alignItems: "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: { xs: "18px 20px", md: "22px 24px" },
padding: "22px 24px",
display: "flex",
flexDirection: "column",
gap: "4px",
alignItems: "center",
gap: "16px",
boxShadow: cardShadow,
mb: "20px",
},
@@ -148,15 +148,10 @@ export const refinedStyles = (
boxShadow: `0 0 0 4px ${toneSoft(tone, tokens)}`,
flexShrink: 0,
}),
heroRow: {
display: "flex",
alignItems: "center",
gap: { xs: "12px", md: "16px" },
},
statusCopy: { flex: 1, minWidth: 0 },
heroTitle: {
m: 0,
flex: 1,
minWidth: 0,
mb: "2px",
fontSize: 17,
fontWeight: 600,
letterSpacing: "-0.01em",
@@ -164,21 +159,14 @@ 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: { xs: "center", md: "flex-end" },
mb: "12px",
},
chartSwitchWrap: { display: "flex", justifyContent: "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,
@@ -186,15 +174,11 @@ 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: { xs: "5px 6px", md: "5px 14px" },
padding: "5px 14px",
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
borderRadius: "5px",
@@ -223,21 +207,13 @@ export const refinedStyles = (
},
cardRow: {
display: "grid",
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" },
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "16px",
p: "16px 20px",
},
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
cardLeft: { minWidth: 0 },
monitorName: {
gridArea: "name",
minWidth: 0,
width: { xs: "100%", md: "auto" },
fontWeight: 600,
fontSize: 14,
letterSpacing: "-0.005em",
@@ -247,9 +223,10 @@ export const refinedStyles = (
whiteSpace: "nowrap",
},
monitorMeta: {
gridArea: "meta",
display: "flex",
gap: "10px",
alignItems: { xs: "flex-start", md: "center" },
alignItems: "center",
mt: "4px",
flexWrap: "wrap",
},
pill: {
@@ -274,8 +251,6 @@ export const refinedStyles = (
},
badge: (tone) => ({
gridArea: "status",
justifySelf: "start",
fontSize: 11,
fontWeight: 600,
padding: "4px 10px",
@@ -286,10 +261,10 @@ export const refinedStyles = (
}),
heatmap: {
padding: { xs: "0 10px 14px", md: "0 20px 16px" },
padding: "0 20px 16px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
height: 42,
},
heatmapCell: (kind) => ({
@@ -301,10 +276,10 @@ export const refinedStyles = (
}),
histogram: {
padding: { xs: "0 10px", md: "0 20px" },
padding: "0 20px",
display: "grid",
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
gap: { xs: "1px", md: "3px" },
gap: "3px",
alignItems: "flex-end",
height: 42,
},
@@ -27,7 +27,6 @@ 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"] };
@@ -102,10 +101,8 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
return (
<Box sx={styles.page}>
{statusPage.customCSS && <style>{statusPage.customCSS}</style>}
<Stack
<Box
component="header"
direction={{ xs: "column", md: "row" }}
sx={styles.top}
>
<HeaderSlot
@@ -115,7 +112,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
monitorCount={monitors.length}
styles={styles}
/>
</Stack>
</Box>
<HeroSlot
statusPage={statusPage}
@@ -164,7 +161,6 @@ 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
@@ -173,39 +169,32 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
sx={styles.card}
>
<Box sx={styles.cardRow}>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<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
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 && (
@@ -191,47 +191,9 @@ const editorial: ThemeVariants = {
},
};
const minimal: ThemeVariants = {
light: {
bg: "#ffffff",
surface: "#ffffff",
border: "#dfe3e8",
text: "#17191c",
textMuted: "#667085",
up: "#12a174",
upStrong: "#087a57",
upSoft: "#e7f7f1",
degraded: "#b54708",
degradedSoft: "#fff1dc",
down: "#c4323f",
downSoft: "#fde8ea",
warn: "#c27803",
warnSoft: "#fff3d6",
radius: "6px",
},
dark: {
bg: "#0d0f12",
surface: "#111419",
border: "#2b3038",
text: "#f2f4f7",
textMuted: "#98a2b3",
up: "#32d6a0",
upStrong: "#1eb783",
upSoft: "rgba(50,214,160,0.14)",
degraded: "#f0a44b",
degradedSoft: "rgba(240,164,75,0.16)",
down: "#ff6675",
downSoft: "rgba(255,102,117,0.16)",
warn: "#f2b84b",
warnSoft: "rgba(242,184,75,0.16)",
radius: "6px",
},
};
export const themeTokens: Record<StatusPageTheme, ThemeVariants> = {
refined,
modern,
bold,
editorial,
minimal,
};
@@ -11,9 +11,7 @@ import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { getStatusPagePublicPath, getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
import { type StatusPage } from "@/Types/StatusPage";
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
interface StatusPagesTableProps {
data: StatusPage[];
@@ -55,7 +53,8 @@ export const StatusPagesTable = ({
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
if (row.isPublished) {
e.stopPropagation();
window.open(getStatusPagePublicUrl(row), "_blank", "noopener,noreferrer");
const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`;
window.open(url, "_blank", "noopener,noreferrer");
}
};
@@ -71,9 +70,7 @@ export const StatusPagesTable = ({
content: t("pages.statusPages.table.headers.url"),
render: (row) => {
const content = row.isPublished
? row.customDomain
? getStatusPagePublicPath(row)
: `/${row.url}`
? `/${row.url}`
: t("pages.statusPages.table.unpublished");
return (
<Stack
@@ -25,8 +25,6 @@ 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[];
@@ -261,24 +259,6 @@ 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"),
@@ -324,9 +304,7 @@ export const MonitorTable = ({
</Stack>
),
render: (row) => {
return row.type === "http" && row.method
? `${row.type} (${row.method})`
: row.type;
return row.type;
},
},
{
-16
View File
@@ -54,24 +54,8 @@ 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
-5
View File
@@ -23,11 +23,6 @@ export interface V8HeapStats {
export interface MongoStats {
readyState: number;
readsPerSecond: number;
insertsPerSecond: number;
updatesPerSecond: number;
deletesPerSecond: number;
writesPerSecond: number;
host: string;
port: number;
dbName: string;
-6
View File
@@ -48,10 +48,6 @@ export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export const HttpMethods = ["GET", "HEAD"] as const;
export type HttpMethod = (typeof HttpMethods)[number];
export const DefaultHttpMethod: HttpMethod = "GET";
export interface Monitor {
id: string;
userId: string;
@@ -68,7 +64,6 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
method?: HttpMethod;
url: string;
port?: number;
isActive: boolean;
@@ -76,7 +71,6 @@ export interface Monitor {
uptimePercentage?: number;
notifications: string[];
tags: string[];
customUpCodes?: number[];
secret?: string;
cpuAlertThreshold: number;
cpuAlertCounter: number;
-2
View File
@@ -9,7 +9,6 @@ export const NotificationChannels = [
"telegram",
"pushover",
"twilio",
"ntfy",
] as const;
export type NotificationChannel = (typeof NotificationChannels)[number];
@@ -26,7 +25,6 @@ export interface Notification {
accessToken?: string;
accountSid?: string;
twilioPhoneNumber?: string;
topic?: string;
createdAt: string;
updatedAt: string;
}
+13 -19
View File
@@ -1,14 +1,5 @@
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;
@@ -18,32 +9,35 @@ export interface QueueJobFailure {
failReason: string | null;
}
export type QueueMetrics = {
export interface QueueMetrics {
jobs: number;
activeJobs: number;
failingJobs: number;
jobsWithFailures: QueueJobFailure[];
totalRuns: number;
totalFailures: number;
workers: QueueWorker[];
};
}
export type QueueJobSummary = {
export interface QueueJobSummary {
monitorId: string | number;
monitorType: string | null;
monitorUrl: string | null;
monitorType: MonitorType | null;
monitorInterval: number | null;
monitorGeoInterval: number | null;
monitorActive: boolean | null;
lockedBy: string | null;
lockedUntil: number | null;
nextScheduledAt: number;
active: boolean;
lockedAt: number | null;
runCount: number;
failCount: number;
failReason: string | null;
lastRunAt: number | null;
lastFinishedAt: number | null;
};
lastRunTook: number | null;
lastFailedAt: number | null;
repeat: number | null;
}
export interface QueueData {
jobs: QueueJobSummary[];
count: number;
metrics: QueueMetrics;
}
+1 -8
View File
@@ -21,13 +21,7 @@ export const getMonitorTypeLabel = (
return key ? t(key) : type;
};
export const STATUS_PAGE_THEMES = [
"refined",
"modern",
"bold",
"editorial",
"minimal",
] as const;
export const STATUS_PAGE_THEMES = ["refined", "modern", "bold", "editorial"] as const;
export type StatusPageTheme = (typeof STATUS_PAGE_THEMES)[number];
export const DEFAULT_STATUS_PAGE_THEME: StatusPageTheme = "refined";
@@ -53,7 +47,6 @@ export interface StatusPage {
type: MonitorDisplayType[];
companyName: string;
url: string;
customDomain?: string | null;
timezone?: string;
color: string;
monitors: string[];
-10
View File
@@ -63,16 +63,6 @@ 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;
-16
View File
@@ -1,16 +0,0 @@
import { isTokenString, isTokenURL, tokenize } from "@csstools/css-tokenizer";
// Mirrors the server check (server/src/utils/customCss.ts) so the form rejects
// the same CSS the API would.
const EXTERNAL_TARGET = /^\s*(?:https?:|\/\/)/i;
export const cssReferencesExternalResource = (css?: string | null): boolean => {
if (typeof css !== "string" || css === "") {
return false;
}
return tokenize({ css }).some(
(token) =>
(isTokenURL(token) || isTokenString(token)) && EXTERNAL_TARGET.test(token[4].value)
);
};
-93
View File
@@ -74,96 +74,3 @@ export const getStatusCodeValueType = (code: number): ValueType => {
if (code < 400) return "neutral";
return "negative";
};
export interface HttpStatusCodeOption {
id: number;
name: string;
}
/**
* Must stay in sync with HttpStatusCodes in server/src/domain/monitors/monitor.types.ts
*/
export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [
{ id: 100, name: "100 - Continue" },
{ id: 101, name: "101 - Switching Protocols" },
{ id: 102, name: "102 - Processing" },
{ id: 103, name: "103 - Early Hints" },
{ id: 200, name: "200 - OK" },
{ id: 201, name: "201 - Created" },
{ id: 202, name: "202 - Accepted" },
{ id: 203, name: "203 - Non-Authoritative Information" },
{ id: 204, name: "204 - No Content" },
{ id: 205, name: "205 - Reset Content" },
{ id: 206, name: "206 - Partial Content" },
{ id: 207, name: "207 - Multi-Status" },
{ id: 208, name: "208 - Already Reported" },
{ id: 226, name: "226 - IM Used" },
{ id: 300, name: "300 - Multiple Choices" },
{ id: 301, name: "301 - Moved Permanently" },
{ id: 302, name: "302 - Found" },
{ id: 303, name: "303 - See Other" },
{ id: 304, name: "304 - Not Modified" },
{ id: 305, name: "305 - Use Proxy" },
{ id: 307, name: "307 - Temporary Redirect" },
{ id: 308, name: "308 - Permanent Redirect" },
{ id: 400, name: "400 - Bad Request" },
{ id: 401, name: "401 - Unauthorized" },
{ id: 402, name: "402 - Payment Required" },
{ id: 403, name: "403 - Forbidden" },
{ id: 404, name: "404 - Not Found" },
{ id: 405, name: "405 - Method Not Allowed" },
{ id: 406, name: "406 - Not Acceptable" },
{ id: 407, name: "407 - Proxy Authentication Required" },
{ id: 408, name: "408 - Request Timeout" },
{ id: 409, name: "409 - Conflict" },
{ id: 410, name: "410 - Gone" },
{ id: 411, name: "411 - Length Required" },
{ id: 412, name: "412 - Precondition Failed" },
{ id: 413, name: "413 - Payload Too Large" },
{ id: 414, name: "414 - URI Too Long" },
{ id: 415, name: "415 - Unsupported Media Type" },
{ id: 416, name: "416 - Range Not Satisfiable" },
{ id: 417, name: "417 - Expectation Failed" },
{ id: 418, name: "418 - I'm a Teapot" },
{ id: 419, name: "419 - Page Expired (Laravel)" },
{ id: 420, name: "420 - Enhance Your Calm (Twitter)" },
{ id: 421, name: "421 - Misdirected Request" },
{ id: 422, name: "422 - Unprocessable Entity" },
{ id: 423, name: "423 - Locked" },
{ id: 424, name: "424 - Failed Dependency" },
{ id: 425, name: "425 - Too Early" },
{ id: 426, name: "426 - Upgrade Required" },
{ id: 428, name: "428 - Precondition Required" },
{ id: 429, name: "429 - Too Many Requests" },
{ id: 431, name: "431 - Request Header Fields Too Large" },
{ id: 440, name: "440 - Login Time-out (IIS)" },
{ id: 449, name: "449 - Retry With (IIS)" },
{ id: 451, name: "451 - Unavailable For Legal Reasons" },
{ id: 460, name: "460 - Client Closed Connection (AWS ELB)" },
{ id: 463, name: "463 - X-Forwarded-For Too Large (AWS ELB)" },
{ id: 497, name: "497 - HTTP Request Sent to HTTPS Port (NGINX)" },
{ id: 499, name: "499 - Client Closed Request (NGINX)" },
{ id: 500, name: "500 - Internal Server Error" },
{ id: 501, name: "501 - Not Implemented" },
{ id: 502, name: "502 - Bad Gateway" },
{ id: 503, name: "503 - Service Unavailable" },
{ id: 504, name: "504 - Gateway Timeout" },
{ id: 505, name: "505 - HTTP Version Not Supported" },
{ id: 506, name: "506 - Variant Also Negotiates" },
{ id: 507, name: "507 - Insufficient Storage" },
{ id: 508, name: "508 - Loop Detected" },
{ id: 509, name: "509 - Bandwidth Limit Exceeded (Apache)" },
{ id: 510, name: "510 - Not Extended" },
{ id: 511, name: "511 - Network Authentication Required" },
{ id: 520, name: "520 - Web Server Returned an Unknown Error (Cloudflare)" },
{ id: 521, name: "521 - Web Server Is Down (Cloudflare)" },
{ id: 522, name: "522 - Connection Timed Out (Cloudflare)" },
{ id: 523, name: "523 - Origin Is Unreachable (Cloudflare)" },
{ id: 524, name: "524 - A Timeout Occurred (Cloudflare)" },
{ id: 525, name: "525 - SSL Handshake Failed (Cloudflare)" },
{ id: 526, name: "526 - Invalid SSL Certificate (Cloudflare)" },
{ id: 527, name: "527 - Railgun Error (Cloudflare)" },
{ id: 529, name: "529 - Site is overloaded" },
{ id: 530, name: "530 - Site is frozen (Cloudflare)" },
{ id: 561, name: "561 - Unauthorized (AWS ELB)" },
];
-99
View File
@@ -1,99 +0,0 @@
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}`;
};
// Same-origin URL for the admin preview iframe; a custom domain would be cross
// origin and blocked by the admin document's CSP frame-src.
export const getStatusPagePreviewUrl = (statusPage: Pick<StatusPage, "url">): string => {
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;
};
+5 -25
View File
@@ -1,13 +1,7 @@
import { z } from "zod";
import type { FieldPath } from "react-hook-form";
import { GeoContinents } from "@/Types/GeoCheck";
import {
DnsRecordTypes,
HttpMethods,
PageSpeedStrategies,
type MonitorType,
} from "@/Types/Monitor";
import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode";
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
@@ -59,15 +53,9 @@ const geoCheckFields = {
};
// HTTP monitor schema
const httpStatusCodeSet = new Set(ALL_HTTP_STATUS_CODES.map((c) => c.id));
const httpStatusCode = z.number().refine((code) => httpStatusCodeSet.has(code), {
message: "Must be a valid HTTP status code",
});
const httpSchema = baseSchema.extend({
type: z.literal("http"),
url: urlSchema,
method: z.enum(HttpMethods).optional().register(monitorStepRegistry, { step: 2 }),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
useAdvancedMatching: z.boolean().register(monitorStepRegistry, { step: 2 }),
matchMethod: z
@@ -76,10 +64,6 @@ const httpSchema = baseSchema.extend({
.register(monitorStepRegistry, { step: 2 }),
expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }),
jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }),
customUpCodes: z
.array(httpStatusCode)
.optional()
.register(monitorStepRegistry, { step: 2 }),
...geoCheckFields,
});
@@ -144,23 +128,19 @@ 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")
.register(monitorStepRegistry, { step: 1 }),
.max(100, "CPU threshold must be at most 100"),
memoryAlertThreshold: z
.number()
.min(0, "Memory threshold must be at least 0")
.max(100, "Memory threshold must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
.max(100, "Memory threshold must be at most 100"),
diskAlertThreshold: z
.number()
.min(0, "Disk threshold must be at least 0")
.max(100, "Disk threshold must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
.max(100, "Disk threshold must be at most 100"),
tempAlertThreshold: z
.number()
.min(0, "Temperature threshold must be at least 0")
.max(150, "Temperature threshold must be at most 150")
.register(monitorStepRegistry, { step: 1 }),
.max(150, "Temperature threshold must be at most 150"),
selectedDisks: z.array(z.string()),
});
-7
View File
@@ -70,12 +70,6 @@ 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,
@@ -87,7 +81,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
telegramSchema,
pushoverSchema,
twilioSchema,
ntfySchema,
]);
export type NotificationFormData = z.infer<typeof notificationSchema>;
+9 -76
View File
@@ -1,31 +1,5 @@
import { z } from "zod";
import type { FieldPath } from "react-hook-form";
import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage";
import { cssReferencesExternalResource } from "@/Utils/customCss";
// 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
@@ -40,68 +14,27 @@ 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").register(statusPageStepRegistry, {
step: 1,
}),
color: z.string().min(1, "Color is required"),
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
isPublished: z.boolean(),
showCharts: z.boolean().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()
.max(100000, "Custom CSS must be at most 100000 characters")
.refine((css) => !cssReferencesExternalResource(css), {
message: "Custom CSS cannot reference external URLs or use @import",
})
.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 }),
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(),
logo: z
.object({
data: z.string(),
contentType: z.string(),
})
.nullable()
.optional()
.register(statusPageStepRegistry, { step: 1 }),
.optional(),
});
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;
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "معرّف المراقب",
"runCount": "عدد التشغيلات",
"failCount": "عدد حالات الفشل",
"lastRunAt": "آخر تشغيل في",
"lockedAt": "مقفل في",
"lastFinishedAt": "آخر انتهاء في",
"lastRunTook": "مدة آخر تشغيل",
"lastFailedAt": "آخر فشل في",
"failReason": "سبب الفشل",
"state": "الحالة"
+3
View File
@@ -945,7 +945,10 @@
"monitorId": "ID del monitor",
"runCount": "Recompte d'execucions",
"failCount": "Recompte",
"lastRunAt": "Darrera execució",
"lockedAt": "Bloquejat el",
"lastFinishedAt": "Darrera finalització",
"lastRunTook": "Durada de la darrera execució",
"lastFailedAt": "Darrera fallada",
"failReason": "Motiu de la fallada",
"state": "Estat"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID monitoru",
"runCount": "Počet spuštění",
"failCount": "Počet selhání",
"lastRunAt": "Poslední spuštění",
"lockedAt": "Uzamčeno v",
"lastFinishedAt": "Naposledy dokončeno",
"lastRunTook": "Poslední běh trval",
"lastFailedAt": "Poslední selhání",
"failReason": "Důvod selhání",
"state": "Stav"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "Monitor-ID",
"runCount": "Ausführungen",
"failCount": "Fehlerzahl",
"lastRunAt": "Letzter Lauf am",
"lockedAt": "Gesperrt am",
"lastFinishedAt": "Zuletzt abgeschlossen am",
"lastRunTook": "Letzter Lauf dauerte",
"lastFailedAt": "Letzter Fehler am",
"failReason": "Fehlergrund",
"state": "Status"
+4 -58
View File
@@ -490,13 +490,6 @@
"advanced": {
"description": "Optional settings for advanced use cases",
"option": {
"method": {
"label": "Request method",
"description": {
"GET": "GET fetches the full response including the body. Advanced matching fully supported",
"HEAD": "HEAD fetches only response headers for reducing load on the target. Supports status code matching but not advanced matching as there is no body."
}
},
"advancedMatching": {
"label": "Use advanced matching"
},
@@ -512,11 +505,6 @@
"equal": "Equal",
"include": "Include",
"regex": "Regex"
},
"customUpCodes": {
"label": "Accepted status codes",
"description": "Add any non-2xx codes that should also be considered successful.",
"removeAriaLabel": "Remove status code"
}
},
"title": "Advanced settings"
@@ -945,8 +933,6 @@
"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",
@@ -987,11 +973,6 @@
},
"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",
@@ -1019,11 +1000,6 @@
},
"table": {
"headers": {
"host": "Host",
"processId": "Process ID",
"workerId": "Worker ID",
"workerMode": "Worker mode",
"lastSeenAt": "Last seen at",
"state": "State",
"timestamp": "Timestamp",
"level": "Level",
@@ -1032,9 +1008,10 @@
"monitorId": "Monitor ID",
"runCount": "Run count",
"failCount": "Count",
"nextScheduledAt": "Next run at",
"lockedBy": "Locked by",
"lastRunAt": "Last run at",
"lockedAt": "Locked at",
"lastFinishedAt": "Last finished at",
"lastRunTook": "Last run took",
"lastFailedAt": "Last failed",
"failReason": "Fail reason"
}
@@ -1180,14 +1157,6 @@
"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": {
@@ -1524,11 +1493,6 @@
"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."
}
}
},
@@ -1585,10 +1549,6 @@
"editorial": {
"name": "Editorial",
"description": "Serif headings and hairline dividers. Distinctive and calm."
},
"minimal": {
"name": "Minimal",
"description": "Flat, focused, and compact with only the essentials."
}
}
},
@@ -1598,16 +1558,6 @@
"light": "Light only",
"dark": "Dark only"
},
"customCSS": {
"title": "Custom CSS",
"description": "Add custom CSS to fine-tune the look of your public status page.",
"option": {
"customCSS": {
"label": "Custom CSS",
"placeholder": "footer { display: none; }"
}
}
},
"features": {
"title": "Features",
"description": "Configure what information is displayed on your status page.",
@@ -1630,9 +1580,6 @@
"header": {
"title": "Status pages",
"description": "Publish public pages that show real-time uptime and incident history to your customers and stakeholders."
},
"preview": {
"title": "Status page preview"
}
},
"uptime": {
@@ -1644,8 +1591,7 @@
},
"table": {
"headers": {
"responseTime": "Response time",
"uptime": "Uptime %"
"responseTime": "Response time"
}
},
"fallback": {
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID de monitor",
"runCount": "Conteo de ejecuciones",
"failCount": "Conteo de fallas",
"lastRunAt": "Última ejecución",
"lockedAt": "Bloqueado a las",
"lastFinishedAt": "Última finalización",
"lastRunTook": "Última ejecución tomó",
"lastFailedAt": "Última falla",
"failReason": "Razón de falla",
"state": "Estado"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "Monitorin tunnus",
"runCount": "Suorituskerrat",
"failCount": "Epäonnistumiskerrat",
"lastRunAt": "Viimeisin suoritus",
"lockedAt": "Lukittu",
"lastFinishedAt": "Viimeisin valmistuminen",
"lastRunTook": "Viimeisin suoritus kesti",
"lastFailedAt": "Viimeisin epäonnistuminen",
"failReason": "Epäonnistumisen syy",
"state": "Tila"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID du moniteur",
"runCount": "Nombre d'exécutions",
"failCount": "Nombre d'échecs",
"lastRunAt": "Dernière exécution",
"lockedAt": "Verrouillé le",
"lastFinishedAt": "Dernière fin",
"lastRunTook": "Durée dernière exécution",
"lastFailedAt": "Dernier échec",
"failReason": "Raison de l'échec",
"state": "État"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "モニターID",
"runCount": "実行回数",
"failCount": "失敗回数",
"lastRunAt": "最終実行日時",
"lockedAt": "ロック日時",
"lastFinishedAt": "最終完了日時",
"lastRunTook": "最終実行時間",
"lastFailedAt": "最終失敗日時",
"failReason": "失敗理由",
"state": "状態"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID do monitor",
"runCount": "Contagem de execuções",
"failCount": "Contagem de falhas",
"lastRunAt": "Última execução em",
"lockedAt": "Bloqueado em",
"lastFinishedAt": "Última conclusão em",
"lastRunTook": "Última execução durou",
"lastFailedAt": "Última falha em",
"failReason": "Motivo da falha",
"state": "Estado"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID монитора",
"runCount": "Кол-во запусков",
"failCount": "Кол-во ошибок",
"lastRunAt": "Последний запуск",
"lockedAt": "Заблокировано в",
"lastFinishedAt": "Последнее завершение",
"lastRunTook": "Последний запуск занял",
"lastFailedAt": "Последняя ошибка",
"failReason": "Причина ошибки",
"state": "Состояние"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "รหัสมอนิเตอร์",
"runCount": "จำนวนรัน",
"failCount": "จำนวนล้มเหลว",
"lastRunAt": "รันล่าสุดเมื่อ",
"lockedAt": "ล็อกเมื่อ",
"lastFinishedAt": "เสร็จสิ้นล่าสุดเมื่อ",
"lastRunTook": "รันล่าสุดใช้เวลา",
"lastFailedAt": "ล้มเหลวล่าสุดเมื่อ",
"failReason": "สาเหตุที่ล้มเหลว",
"state": "สถานะ"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "Monitör ID",
"runCount": "Çalışma sayısı",
"failCount": "Başarısızlık sayısı",
"lastRunAt": "Son çalışma zamanı",
"lockedAt": "Kilitlenme zamanı",
"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",
"state": "Durum"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID монітора",
"runCount": "Кількість запусків",
"failCount": "Кількість помилок",
"lastRunAt": "Останній запуск",
"lockedAt": "Заблоковано о",
"lastFinishedAt": "Останнє завершення",
"lastRunTook": "Останній запуск тривав",
"lastFailedAt": "Остання помилка",
"failReason": "Причина помилки",
"state": "Стан"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "ID trình giám sát",
"runCount": "Số lần chạy",
"failCount": "Số lần lỗi",
"lastRunAt": "Chạy lần cuối lúc",
"lockedAt": "Khóa lúc",
"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",
"state": "Trạng thái"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "监控 ID",
"runCount": "运行次数",
"failCount": "失败次数",
"lastRunAt": "上次运行于",
"lockedAt": "锁定于",
"lastFinishedAt": "上次完成于",
"lastRunTook": "上次运行耗时",
"lastFailedAt": "上次失败于",
"failReason": "失败原因",
"state": "状态"
+3
View File
@@ -941,7 +941,10 @@
"monitorId": "監控 ID",
"runCount": "執行次數",
"failCount": "失敗次數",
"lastRunAt": "最後執行於",
"lockedAt": "鎖定於",
"lastFinishedAt": "最後完成於",
"lastRunTook": "最後執行耗時",
"lastFailedAt": "最後失敗於",
"failReason": "失敗原因",
"state": "狀態"
+4
View File
@@ -2,10 +2,14 @@
!quickstart.sh
!build_images.sh
dev/mongo/data/*
dev/redis/data/*
dist/mongo/data/*
dist/redis/data/*
prod/mongo/data/*
prod/redis/data/*
dist/docker-compose-test.yaml
/dist-mono/mongo/data/*
/dist-mono/redis/data/*
/dist-arm/mongo/data/*
*.env
+6 -3
View File
@@ -12,11 +12,14 @@ set dockerfiles[0]=.\docker\coolify\client.Dockerfile
set services[1]=bluewaveuptime/uptime_database_mongo
set dockerfiles[1]=.\docker\coolify\mongoDB.Dockerfile
set services[2]=bluewaveuptime/uptime_server
set dockerfiles[2]=.\docker\coolify\server.Dockerfile
set services[2]=bluewaveuptime/uptime_redis
set dockerfiles[2]=.\docker\coolify\redis.Dockerfile
set services[3]=bluewaveuptime/uptime_server
set dockerfiles[3]=.\docker\coolify\server.Dockerfile
REM Loop through each service and build the corresponding image
for /L %%i in (0,1,2) do (
for /L %%i in (0,1,2,3) do (
set service=!services[%%i]!
set dockerfile=!dockerfiles[%%i]!
+1
View File
@@ -7,6 +7,7 @@ cd ../../
declare -A services=(
["bluewaveuptime/uptime_client"]="./docker/coolify/client.Dockerfile"
["bluewaveuptime/uptime_database_mongo"]="./docker/coolify/mongoDB.Dockerfile"
["bluewaveuptime/uptime_redis"]="./docker/coolify/redis.Dockerfile"
["bluewaveuptime/uptime_server"]="./docker/coolify/server.Dockerfile"
)
+2
View File
@@ -0,0 +1,2 @@
FROM redis
EXPOSE 6379
@@ -19,6 +19,7 @@ services:
# Mount the certs directory (read-only for security)
- ./certs:/certs:ro
depends_on:
- redis
- mongodb
# Optional: Add healthcheck to ensure the server starts properly
healthcheck:
-26
View File
@@ -20,35 +20,9 @@ services:
- server.env
depends_on:
- mongodb
worker:
image: uptime_server:latest
restart: always
ports:
- "52346:52346"
env_file:
- server.env
environment:
QUEUE_MODE: "worker"
HEALTH_PORT: "52346"
stop_grace_period: 30s
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:52346/livez',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))",
]
interval: 15s
timeout: 3s
retries: 3
depends_on:
- mongodb
mongodb:
image: uptime_mongo:latest
restart: always
ports:
- "27017:27017"
command: ["mongod", "--quiet", "--bind_ip_all"]
volumes:
- ./mongo/data:/data/db
+1
View File
@@ -9,6 +9,7 @@ services:
- UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1
- UPTIME_APP_CLIENT_HOST=http://localhost
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
- REDIS_URL=redis://redis:6379
- CLIENT_HOST=http://localhost
- JWT_SECRET=my_secret
depends_on:
+3 -3
View File
@@ -63,9 +63,9 @@ Checkmate ha sido sometido a pruebas de carga con más de 1000 monitores activos
<a id="demo"></a>
## Demo
Puedes ver la última versión de [Checkmate](https://demo.checkmate.so/) en acción.
Puedes probar la última versión de [Checkmate](https://demo.checkmate.so/) en directo.
El usuario es demouser@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando; si no funciona, escríbenos en el canal Discussions).
El usuario es demouser@demo.com y la contraseña Demouser1!. (Actualizamos el servidor de demostración cada cierto tiempo; si no funciona, escríbenos por el canal Discussions.)
<a id="users-guide"></a>
## Guía de usuario
@@ -92,7 +92,7 @@ Para más documentación, consulta el [directorio docs](../).
<a id="performance"></a>
## Rendimiento
Gracias a optimizaciones extensas, Checkmate funciona con una huella de memoria excepcionalmente baja y requiere un mínimo de memoria y CPU. A continuación se muestra el uso de memoria de una instancia de Node.js en un servidor que monitoriza 323 servidores cada minuto:
Gracias a múltiples optimizaciones, Checkmate funciona con una huella de memoria muy baja y necesita muy poca RAM y CPU. Aquí tienes el uso de memoria de una instancia de Node.js en un servidor que monitoriza 323 servidores cada minuto:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
+29 -29
View File
@@ -40,11 +40,11 @@
<br>
Ce dépôt contient à la fois le frontend et le backend de Checkmate, un outil de surveillance open source et auto-hébergeable permettant de suivre le matériel des serveurs, la disponibilité, les temps de réponse et les incidents en temps réel grâce à de belles visualisations. Checkmate vérifie régulièrement la disponibilité et le bon fonctionnement de vos serveurs ou sites web, et fournit des alertes et rapports en temps réel sur la disponibilité, les pannes et les temps de réponse des services surveillés.
Ce dépôt contient le frontend et le backend de Checkmate, un outil de monitoring open source et auto-hébergeable qui suit en temps réel le matériel des serveurs, la disponibilité, les temps de réponse et les incidents avec des visualisations soignées. Checkmate vérifie périodiquement si vos serveurs ou sites web sont accessibles et performants, et envoie alertes et rapports en temps réel sur leur disponibilité, leurs interruptions et leurs temps de réponse.
Checkmate dispose aussi d'un agent, appelé [Capture](https://github.com/bluewave-labs/capture), pour récupérer des données depuis des serveurs distants. Capture n'est pas requis pour faire tourner Checkmate, mais il apporte des informations supplémentaires sur l'état du CPU, de la RAM, du disque et de la température de vos serveurs. Capture fonctionne sur Linux, Windows, Mac, Raspberry Pi ou tout appareil capable d'exécuter Go.
Checkmate s'accompagne d'un agent appelé [Capture](https://github.com/bluewave-labs/capture), qui collecte des données depuis des serveurs distants. Capture n'est pas obligatoire, mais il apporte des informations supplémentaires sur le CPU, la RAM, le disque et la température de vos serveurs. Il tourne sous Linux, Windows, macOS, Raspberry Pi, ou n'importe quel appareil capable d'exécuter Go.
Checkmate a été testé en charge avec plus de 1 000 moniteurs actifs sans aucun problème ni goulet d'étranglement notable.
Checkmate a été testé sous charge avec plus de 1 000 moniteurs actifs, sans problème ni baisse de performance notable.
## 📚 Table des matières
@@ -63,9 +63,9 @@ Checkmate a été testé en charge avec plus de 1 000 moniteurs actifs sans aucu
<a id="demo"></a>
## Démo
Vous pouvez voir la dernière version de [Checkmate](https://demo.checkmate.so/) en action.
La dernière version de [Checkmate](https://demo.checkmate.so/) est consultable en ligne.
L'identifiant est demouser@demo.com et le mot de passe Demouser1! (Note : nous mettons à jour le serveur de démo de temps en temps ; si cela ne fonctionne pas, faites-nous signe sur le canal Discussions).
L'identifiant est demouser@demo.com et le mot de passe Demouser1!. (Le serveur de démo est mis à jour de temps en temps ; s'il est indisponible, dites-le-nous sur le canal Discussions.)
<a id="users-guide"></a>
## Guide utilisateur
@@ -85,14 +85,14 @@ Vous pouvez également utiliser [Coolify](https://coolify.io/), [Elestio](https:
### Utiliser une autorité de certification personnalisée
Si vous devez surveiller des points d'accès HTTPS internes avec des certificats issus d'autorités de certification privées (comme Smallstep), consultez notre [guide de confiance d'autorité de certification personnalisée](../custom-ca-trust.md) pour les options de configuration Docker.
Si vous devez surveiller des endpoints HTTPS internes dont les certificats sont signés par une autorité de certification privée (comme Smallstep), consultez notre [guide d'autorité de certification personnalisée](../custom-ca-trust.md) — il décrit les options de configuration Docker.
Pour plus de documentation, consultez le [répertoire docs](../).
Pour plus de documentation, voyez le [répertoire docs](../).
<a id="performance"></a>
## Performance
Grâce à de nombreuses optimisations, Checkmate fonctionne avec une empreinte mémoire exceptionnellement faible, nécessitant un minimum de RAM et de CPU. Voici l'utilisation mémoire d'une instance Node.js sur un serveur qui surveille 323 serveurs chaque minute :
Grâce à de nombreuses optimisations, Checkmate fonctionne avec une empreinte mémoire exceptionnellement faible et n'a besoin que de très peu de RAM et de CPU. Voici l'utilisation mémoire d'une instance Node.js sur un serveur qui surveille 323 serveurs chaque minute :
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
@@ -108,21 +108,21 @@ Si vous avez des questions, suggestions ou commentaires, plusieurs options s'off
- [Canal Discord](https://discord.gg/NAb6H3UTjK) (préféré)
- [Discussions GitHub](https://github.com/bluewave-labs/Checkmate/discussions) (nous y passons de temps en temps)
N'hésitez pas à poser vos questions ou partager vos idées — nous adorerions vous entendre !
N'hésitez pas à poser vos questions ou à partager vos idées — vos retours nous intéressent !
<a id="features"></a>
## Fonctionnalités
- Entièrement open source, déployable sur vos serveurs ou appareils personnels (par ex. Raspberry Pi 4 ou 5)
- Plusieurs options de surveillance : disponibilité, Docker, ping, SSL, port, serveur de jeu
- Surveillance de la vitesse des pages
- Surveillance de l'infrastructure (mémoire, utilisation du disque, performance CPU, réseau, etc.) — nécessite l'agent [Capture](https://github.com/bluewave-labs/capture)
- Surveillance sélective du disque avec choix de points de montage
- Plusieurs types de monitoring : uptime, Docker, ping, SSL, port, serveur de jeu
- Monitoring de la vitesse des pages
- Monitoring d'infrastructure (mémoire, disque, CPU, réseau, etc.) — nécessite l'agent [Capture](https://github.com/bluewave-labs/capture)
- Monitoring de disque sélectif via choix des points de montage
- Incidents en un coup d'œil
- Pages de statut avec 4 thèmes élégants
- Pages de statut avec 4 thèmes soignés
- Notifications par e-mail, webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS)
- Maintenance planifiée
- Surveillance par requête JSON
- Monitoring via requête JSON
- Support multilingue : arabe, chinois (simplifié), chinois (traditionnel, Taïwan), tchèque, anglais, finnois, français, allemand, japonais, portugais (Brésil), russe, espagnol, thaï, turc, ukrainien et vietnamien
@@ -166,29 +166,29 @@ N'hésitez pas à poser vos questions ou partager vos idées — nous adorerions
<a id="a-few-links"></a>
## Quelques liens
- Si vous souhaitez nous soutenir, merci de mettre une ⭐ et de cliquer sur « watch ».
- Une question ou une suggestion pour la roadmap ou les fonctionnalités ? Rejoignez notre [canal Discord](https://discord.gg/NAb6H3UTjK) ou les [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
- Besoin d'être notifié à chaque nouvelle version ? Utilisez [Newreleases](https://newreleases.io/), un service gratuit de suivi des sorties.
- Regardez une [vidéo d'installation et d'utilisation de Checkmate](https://www.youtube.com/watch?v=GfFOc0xHIwY)
- Pour nous soutenir, laissez une ⭐ et cliquez sur « watch ».
- Une question ou une suggestion sur la roadmap ou les fonctionnalités ? Passez par notre [Discord](https://discord.gg/NAb6H3UTjK) ou par les [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
- Vous voulez être prévenu des nouvelles versions ? [Newreleases](https://newreleases.io/) est un service gratuit de suivi des sorties.
- Regardez une [vidéo d'installation et d'utilisation de Checkmate](https://www.youtube.com/watch?v=GfFOc0xHIwY).
<a id="contributing"></a>
## Contribuer
Nous sommes [Alex](http://github.com/ajhollid) (chef d'équipe), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) et [Karen](https://github.com/karenvicent), et nous aidons les particuliers et les entreprises à surveiller leur infrastructure et leurs serveurs.
Nous sommes [Alex](http://github.com/ajhollid) (chef d'équipe), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) et [Karen](https://github.com/karenvicent), et nous aidons particuliers et entreprises à monitorer leur infrastructure et leurs serveurs.
Nous sommes fiers de construire des liens solides avec les contributeurs à tous les niveaux. Bien que jeune, Checkmate a déjà obtenu plus de 7 000 étoiles et attiré plus de 90 contributeurs du monde entier.
Nous tenons à construire des liens solides avec les contributeurs, quel que soit leur niveau. Bien que jeune, Checkmate a déjà dépassé 7 000 étoiles et attiré plus de 90 contributeurs partout dans le monde.
Notre dépôt a été suivi par des employés 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 et NEC** — alors n'hésitez pas, rejoignez-nous, contribuez et apprenez avec nous !
Notre dépôt est suivi par des employés 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 et NEC** — alors lancez-vous, rejoignez-nous, contribuez et apprenez avec nous.
Voici comment contribuer :
Comment contribuer :
0. Mettez une étoile à ce dépôt :)
1. Consultez le [guide du contributeur](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Les nouveaux contributeurs sont encouragés à consulter le tag `good-first-issue`.
2. Lisez une structure détaillée de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si vous souhaitez plonger dans l'architecture.
3. Ouvrez une issue si vous pensez avoir rencontré un bug.
4. Cherchez les `good-first-issue` si vous débutez.
5. Faites une pull request pour ajouter de nouvelles fonctionnalités, des améliorations ou corriger des bugs.
6. Découvrez ce parcours interactif de la base de code `Checkmate` sur CodeCanvas [ici](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Pour affiner les simulations de flux de données existantes ou en créer de nouvelles, suivez le tutoriel rapide [ici](https://docs.code-canvas.com/updating-diagram).
1. Lisez le [guide du contributeur](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Si c'est votre première contribution, jetez d'abord un œil au tag `good-first-issue`.
2. Si vous voulez plonger dans l'architecture, lisez la [structure détaillée de Checkmate](https://deepwiki.com/bluewave-labs/Checkmate).
3. Ouvrez une issue si vous pensez avoir trouvé un bug.
4. Pour débuter, cherchez les tickets marqués `good-first-issue`.
5. Ouvrez une pull request pour ajouter une fonctionnalité, améliorer l'existant ou corriger un bug.
6. Parcourez la base de code `Checkmate` via la visite interactive sur CodeCanvas, [ici](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Pour affiner les simulations de flux de données existantes ou en créer de nouvelles, suivez le tutoriel rapide [ici](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" />
+10 -10
View File
@@ -40,11 +40,11 @@
<br>
このリポジトリには、Checkmate のフロントエンドとバックエンドの両方が含まれています。Checkmate は、サーバーのハードウェア、稼働状況、応答時間、インシデントを美しい可視化でリアルタイムに追跡する、オープンソースかつセルフホスト型の監視ツールです。Checkmate はサーバーや Web サイトにアクセスできるか、最適に動作しているかを定期的にチェックし、監視対象サービスの可用性・ダウンタイム・応答時間に関するリアルタイムのアラートとレポートを提供します。
このリポジトリには、Checkmate のフロントエンドとバックエンドの両方が含まれています。Checkmate は、サーバーのハードウェア、稼働状況、応答時間、インシデントを見やすい可視化でリアルタイムに追跡する、オープンソースかつセルフホスト可能な監視ツールです。Checkmate はサーバーや Web サイトにアクセスできるか、正常に動作しているかを定期的にチェックし、監視対象サービスの可用性・ダウンタイム・応答時間に関するリアルタイムのアラートとレポートを提供します。
Checkmate には、リモートサーバーからデータを取得するためのエージェントとして [Capture](https://github.com/bluewave-labs/capture) もあります。Capture は Checkmate の動作に必須ではありませんが、サーバーの CPU、RAM、ディスク、温度の状態について追加の情報を提供します。Capture は Linux、Windows、Mac、Raspberry Pi、その他 Go を実行できるあらゆるデバイスで動作します。
Checkmate には、リモートサーバーからデータを取得するエージェント [Capture](https://github.com/bluewave-labs/capture) も用意されています。Capture は Checkmate の動作に必須ではありませんが、サーバーの CPU、RAM、ディスク、温度の状態について追加情報を提供します。Capture は Linux、Windows、macOS、Raspberry Pi、その他 Go を実行できるあらゆるデバイスで動作します。
Checkmate は 1000 以上のアクティブなモニターでストレステストされており、目立った問題や性能のボトルネックは発生していません。
Checkmate は 1000 以上のアクティブな監視で負荷テストされており、目立った問題や性能のボトルネックは発生していません。
## 📚 目次
@@ -63,9 +63,9 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
<a id="demo"></a>
## デモ
[Checkmate](https://demo.checkmate.so/) の最新ビルドを実際に確認できます。
[Checkmate](https://demo.checkmate.so/) の最新ビルドをそのまま試せます。
ユーザー名は demouser@demo.com、パスワードは Demouser1! です(注: デモサーバーは時々更新されています。動作しない場合は Discussions チャンネルでお知らせください)。
ユーザー名は demouser@demo.com、パスワードは Demouser1! です。(デモサーバーは時々更新しています。動作しない場合は Discussions チャンネルでお知らせください。)
<a id="users-guide"></a>
## ユーザーガイド
@@ -81,7 +81,7 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
インストール手順は [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 リポジトリにもインストール手順が記載されています。
代わりに、[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 の利用
@@ -92,7 +92,7 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
<a id="performance"></a>
## パフォーマンス
広範な最適化により、Checkmate は非常に小さなメモリフットプリントで動作し、必要なメモリと CPU リソースは最小限です。以下は、毎分 323 台のサーバーを監視するサーバー上の Node.js インスタンスのメモリ使用量です:
広範な最適化により、Checkmate は非常に小さなメモリフットプリントで動作し、必要なメモリと CPU は最小限で済みます。以下は、毎分 323 台のサーバーを監視するサーバー上の Node.js インスタンスのメモリ使用量です:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
@@ -103,12 +103,12 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
<a id="questions--ideas"></a>
## 質問とアイデア
質問・提案・コメントがあれば、いくつかの選択肢があります:
質問・提案・コメントがあれば、いくつかの窓口があります:
- [Discord チャンネル](https://discord.gg/NAb6H3UTjK)(推奨)
- [Discord](https://discord.gg/NAb6H3UTjK)(推奨)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions)(時々確認しています)
質問やアイデアの共有はお気軽にどうぞ — お声をお聞かせいただけると嬉しいです!
質問やアイデアの共有はお気軽にどうぞ — お声をお聞かせいただけると嬉しいです。
<a id="features"></a>
## 機能
+34 -34
View File
@@ -40,11 +40,11 @@
<br>
Este repositório contém o frontend e o backend do Checkmate, uma ferramenta de monitoramento de código aberto e auto-hospedável para acompanhar hardware de servidores, disponibilidade, tempos de resposta e incidentes em tempo real com visualizações bonitas. O Checkmate verifica regularmente se um servidor ou site está acessível e funcionando de forma ótima, oferecendo alertas e relatórios em tempo real sobre disponibilidade, indisponibilidade e tempos de resposta dos serviços monitorados.
Este repositório contém o frontend e o backend do Checkmate, uma ferramenta de monitoramento open source e auto-hospedável que acompanha em tempo real o hardware dos servidores, a disponibilidade, os tempos de resposta e os incidentes — tudo com visualizações bem feitas. O Checkmate verifica periodicamente se um servidor ou site está acessível e funcionando bem, e envia alertas e relatórios em tempo real sobre disponibilidade, indisponibilidade e tempos de resposta dos serviços monitorados.
O Checkmate também possui um agente chamado [Capture](https://github.com/bluewave-labs/capture) para coletar dados de servidores remotos. O Capture não é necessário para rodar o Checkmate, mas fornece informações adicionais sobre o status de CPU, RAM, disco e temperatura dos servidores. O Capture pode rodar em Linux, Windows, Mac, Raspberry Pi ou qualquer dispositivo capaz de executar Go.
O Checkmate também conta com um agente chamado [Capture](https://github.com/bluewave-labs/capture), que coleta dados de servidores remotos. Capture não é obrigatório para rodar o Checkmate, mas dá informações adicionais sobre CPU, RAM, disco e temperatura dos seus servidores. Ele roda em Linux, Windows, macOS, Raspberry Pi ou qualquer dispositivo capaz de executar Go.
O Checkmate passou por testes de estresse com mais de 1000 monitores ativos sem nenhum problema ou gargalo de desempenho notável.
O Checkmate foi testado sob carga com mais de 1.000 monitores ativos, sem problemas nem gargalos de desempenho notáveis.
## 📚 Sumário
@@ -63,14 +63,14 @@ O Checkmate passou por testes de estresse com mais de 1000 monitores ativos sem
<a id="demo"></a>
## Demo
Você pode ver a versão mais recente do [Checkmate](https://demo.checkmate.so/) em ação.
A versão mais recente do [Checkmate](https://demo.checkmate.so/) está disponível para testar.
O usuário é demouser@demo.com e a senha é Demouser1! (Observação: atualizamos o servidor de demonstração de tempos em tempos; se não funcionar, fale conosco no canal Discussions).
O usuário é demouser@demo.com e a senha é Demouser1!. (O servidor de demonstração é atualizado de tempos em tempos; se não estiver no ar, avise a gente no canal Discussions.)
<a id="users-guide"></a>
## Guia do usuário
As instruções de uso podem ser encontradas [aqui](https://checkmate.so/docs).
As instruções de uso estão [aqui](https://checkmate.so/docs).
## Pré-requisitos
- [Docker](https://www.docker.com/) instalado
@@ -79,20 +79,20 @@ As instruções de uso podem ser encontradas [aqui](https://checkmate.so/docs).
<a id="installation"></a>
## Instalação
Veja as instruções de instalação no [portal de documentação do Checkmate](https://checkmate.so/docs).
As instruções de instalação estão no [portal de documentação do Checkmate](https://checkmate.so/docs).
Alternativamente, você pode usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (África do Sul), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) para subir rapidamente uma instância do Checkmate. Se quiser monitorar sua infraestrutura de servidores, você precisará do [agente Capture](https://github.com/bluewave-labs/capture). O repositório do Capture também contém as instruções de instalação.
Como alternativa, você pode usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (África do Sul), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) para subir rapidamente uma instância. Para monitorar a infraestrutura dos seus servidores, é preciso o [agente Capture](https://github.com/bluewave-labs/capture); o repositório do Capture também traz as instruções de instalação.
### Usando uma CA personalizada
### Usando uma CA própria
Se você precisa monitorar endpoints HTTPS internos com certificados emitidos por autoridades certificadoras privadas (como Smallstep), veja nosso [Guia de confiança em CA personalizada](../custom-ca-trust.md) para opções de configuração do Docker.
Se você precisa monitorar endpoints HTTPS internos com certificados de autoridades certificadoras privadas (como Smallstep), confira nosso [guia de confiança em CA personalizada](../custom-ca-trust.md) com as opções de configuração para o Docker.
Para mais documentação, veja o [diretório docs](../).
<a id="performance"></a>
## Desempenho
Graças a otimizações extensas, o Checkmate opera com uma pegada de memória excepcionalmente pequena, exigindo memória e CPU mínimos. Veja o uso de memória de uma instância Node.js em um servidor monitorando 323 servidores a cada minuto:
Graças a muitas otimizações, o Checkmate roda com uma pegada de memória excepcionalmente baixa e precisa de muito pouca memória e CPU. Veja o uso de memória de uma instância Node.js em um servidor monitorando 323 servidores a cada minuto:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
@@ -103,25 +103,25 @@ Você pode ver a pegada de memória do MongoDB e Redis no mesmo servidor (398 MB
<a id="questions--ideas"></a>
## Perguntas & ideias
Se você tem perguntas, sugestões ou comentários, há várias opções:
Se tiver perguntas, sugestões ou comentários, há várias opções:
- [Canal do Discord](https://discord.gg/NAb6H3UTjK) (preferido)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (passamos por aqui de vez em quando)
- [Discord](https://discord.gg/NAb6H3UTjK) (preferido)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (passamos por lá de vez em quando)
Sinta-se à vontade para perguntar ou compartilhar suas ideias — adoramos ouvir você!
Fique à vontade para perguntar ou compartilhar ideias — a gente adora ouvir você!
<a id="features"></a>
## Funcionalidades
- Totalmente código aberto, implantável em seus servidores ou dispositivos domésticos (ex.: Raspberry Pi 4 ou 5)
- Várias opções de monitoramento: disponibilidade, Docker, ping, SSL, porta, servidor de jogos
- Monitoramento de velocidade de página
- Monitoramento de infraestrutura (memória, uso de disco, desempenho de CPU, rede, etc.) — requer o agente [Capture](https://github.com/bluewave-labs/capture)
- Totalmente open source, pode ser instalado nos seus próprios servidores ou em dispositivos domésticos (ex.: Raspberry Pi 4 ou 5)
- Vários tipos de monitoramento: uptime, Docker, ping, SSL, porta, servidor de jogos
- Monitoramento de velocidade de páginas
- Monitoramento de infraestrutura (memória, disco, desempenho de CPU, rede etc.) — requer o agente [Capture](https://github.com/bluewave-labs/capture)
- Monitoramento seletivo de disco com escolha de pontos de montagem
- Incidentes em um relance
- Páginas de status com 4 temas elegantes
- Incidentes num só lugar
- Páginas de status com 4 temas bem feitos
- Notificações por e-mail, webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS)
- Manutenção agendada
- Manutenções agendadas
- Monitoramento por consulta JSON
- Suporte a múltiplos idiomas: árabe, chinês (simplificado), chinês (tradicional, Taiwan), tcheco, inglês, finlandês, francês, alemão, japonês, português (Brasil), russo, espanhol, tailandês, turco, ucraniano e vietnamita
@@ -166,29 +166,29 @@ Sinta-se à vontade para perguntar ou compartilhar suas ideias — adoramos ouvi
<a id="a-few-links"></a>
## Alguns links
- Se quiser nos apoiar, considere dar uma ⭐ e clicar em "watch".
- Tem alguma pergunta ou sugestão sobre o roadmap/funcionalidades? Confira nosso [canal do Discord](https://discord.gg/NAb6H3UTjK) ou o fórum [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
- Quer receber um aviso a cada novo lançamento? Use o [Newreleases](https://newreleases.io/), um serviço gratuito de acompanhamento de releases.
- Para nos apoiar, deixe uma ⭐ e clique em "watch".
- Tem dúvida ou sugestão sobre o roadmap ou as funcionalidades? Aparece no [Discord](https://discord.gg/NAb6H3UTjK) ou no fórum [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
- Quer ser avisado a cada novo lançamento? [Newreleases](https://newreleases.io/) é um serviço gratuito que acompanha releases.
- Assista a um [vídeo de instalação e uso do Checkmate](https://www.youtube.com/watch?v=GfFOc0xHIwY)
<a id="contributing"></a>
## Contribuindo
Somos o [Alex](http://github.com/ajhollid) (líder de equipe), o [Gorkem](http://github.com/gorkem-bwl/), o [Aryaman](https://github.com/Br0wnHammer), o [Mert](https://github.com/mertssmnoglu) e a [Karen](https://github.com/karenvicent) — ajudamos indivíduos e empresas a monitorar suas infraestruturas e servidores.
Somos [Alex](http://github.com/ajhollid) (líder de equipe), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) e [Karen](https://github.com/karenvicent), e ajudamos pessoas e empresas a monitorar a infraestrutura e os servidores delas.
Nos orgulhamos de construir conexões fortes com contribuidores de todos os níveis. Apesar de ser um projeto jovem, o Checkmate já conquistou mais de 7000 estrelas e atraiu mais de 90 contribuidores no mundo todo.
Cuidamos para construir relações sólidas com contribuidores em qualquer nível. Apesar de novo, o Checkmate já passou de 7.000 estrelas e mais de 90 contribuidores ao redor do mundo.
Nosso repositório recebeu estrelas de funcionários do **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes e NEC** — então não se segure: entre, contribua e aprenda com a gente!
Funcionários do **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes e NEC** já deram estrela no nosso repositório — então fique à vontade para entrar, contribuir e aprender com a gente.
Como contribuir:
0. Dê uma estrela neste repositório :)
1. Confira o [guia do contribuidor](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Iniciantes são incentivados a verificar a tag `good-first-issue`.
2. Leia uma estrutura detalhada do [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) se quiser mergulhar na arquitetura.
3. Abra uma issue se acreditar ter encontrado um bug.
4. Procure por `good-first-issue`s se você é novo.
5. Faça um pull request para adicionar novas funcionalidades, melhorias ou correções de bugs.
6. Confira este passo a passo interativo da base de código do `Checkmate` no CodeCanvas [aqui](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Para refinar simulações existentes de fluxo de dados ou criar novas, siga o tutorial rápido [aqui](https://docs.code-canvas.com/updating-diagram).
1. Leia o [guia do contribuidor](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se for sua primeira vez, dê uma olhada na tag `good-first-issue` primeiro.
2. Para mergulhar na arquitetura, leia a [estrutura detalhada do Checkmate](https://deepwiki.com/bluewave-labs/Checkmate).
3. Abra uma issue se achar que encontrou um bug.
4. Se você é novo por aqui, procure pelas tags `good-first-issue`.
5. Abra uma pull request para adicionar funcionalidades, melhorar pequenas coisas ou corrigir bugs.
6. Veja o tour interativo pela base de código do `Checkmate` no CodeCanvas [aqui](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Para refinar simulações de fluxo de dados ou criar novas, siga o tutorial rápido [aqui](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" />
+10 -10
View File
@@ -40,11 +40,11 @@
<br>
В этом репозитории находятся как frontend, так и backend Checkmate — open-source-инструмента самостоятельного хостинга для отслеживания оборудования серверов, времени работы, времени отклика и инцидентов в реальном времени с наглядными визуализациями. Checkmate регулярно проверяет, доступен ли сервер/сайт и работает ли он оптимально, предоставляя оповещения и отчёты о доступности, простое и времени отклика отслеживаемых сервисов в реальном времени.
В этом репозитории — фронтенд и бэкенд Checkmate, open-source и самостоятельно хостящегося инструмента мониторинга, который в реальном времени отслеживает оборудование серверов, доступность, время отклика и инциденты — и показывает всё это в виде понятных графиков. Checkmate периодически проверяет, доступен ли сервер или сайт и работает ли он как надо, и присылает оповещения и отчёты о доступности, простоях и времени отклика отслеживаемых сервисов в реальном времени.
У Checkmate также есть агент под названием [Capture](https://github.com/bluewave-labs/capture) для получения данных с удалённых серверов. Capture не является обязательным для работы Checkmate, но даёт дополнительную информацию о состоянии CPU, RAM, диска и температуры ваших серверов. Capture работает на Linux, Windows, Mac, Raspberry Pi и любом устройстве, способном запускать Go.
У Checkmate также есть агент [Capture](https://github.com/bluewave-labs/capture), который собирает данные с удалённых серверов. Capture не обязателен для работы Checkmate, но даёт дополнительную информацию о состоянии CPU, RAM, диска и температуры ваших серверов. Capture работает на Linux, Windows, macOS, Raspberry Pi и на любом устройстве, способном запускать Go.
Checkmate прошёл нагрузочное тестирование с 1000+ активных мониторов без каких-либо заметных проблем или просадок производительности.
Checkmate прошёл нагрузочное тестирование с 1000+ активных мониторов — без заметных проблем и просадок производительности.
## 📚 Содержание
@@ -63,9 +63,9 @@ Checkmate прошёл нагрузочное тестирование с 1000+
<a id="demo"></a>
## Демо
Последнюю сборку [Checkmate](https://demo.checkmate.so/) можно увидеть в действии.
Последнюю сборку [Checkmate](https://demo.checkmate.so/) можно посмотреть прямо сейчас.
Имя пользователя — demouser@demo.com, пароль — Demouser1! (Обратите внимание: мы периодически обновляем демо-сервер; если что-то не работает, напишите нам на канале Discussions).
Логин — demouser@demo.com, пароль — Demouser1!. (Демо-сервер мы периодически обновляем; если он недоступен, напишите нам в канал Discussions.)
<a id="users-guide"></a>
## Руководство пользователя
@@ -81,7 +81,7 @@ Checkmate прошёл нагрузочное тестирование с 1000+
Инструкции по установке смотрите на [портале документации 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 также есть инструкции по установке.
Также можно использовать [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); инструкции по его установке лежат в его репозитории.
### Использование собственного УЦ
@@ -92,7 +92,7 @@ Checkmate прошёл нагрузочное тестирование с 1000+
<a id="performance"></a>
## Производительность
Благодаря многочисленным оптимизациям Checkmate работает с исключительно низким потреблением памяти и требует минимума RAM и CPU. Ниже — потребление памяти инстансом Node.js на сервере, который мониторит 323 сервера каждую минуту:
Благодаря многочисленным оптимизациям Checkmate работает с очень низким потреблением памяти и требует совсем немного RAM и CPU. Ниже — потребление памяти инстансом Node.js на сервере, который мониторит 323 сервера каждую минуту:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
@@ -103,12 +103,12 @@ Checkmate прошёл нагрузочное тестирование с 1000+
<a id="questions--ideas"></a>
## Вопросы и идеи
Если у вас есть вопросы, предложения или комментарии — есть несколько вариантов:
Если есть вопросы, предложения или замечания — несколько вариантов на выбор:
- [Канал в Discord](https://discord.gg/NAb6H3UTjK) (предпочтительно)
- [Discord](https://discord.gg/NAb6H3UTjK) (предпочтительно)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (заглядываем туда время от времени)
Смело задавайте вопросы и делитесь идеями — мы будем рады услышать вас!
Спрашивайте смело и делитесь идеями — нам интересно ваше мнение.
<a id="features"></a>
## Возможности
+10 -10
View File
@@ -40,11 +40,11 @@
<br>
Bu depo, sunucu donanımını, çalışma süresini, yanıt sürelerini ve olayları zarif görselleştirmelerle gerçek zamanlı takip eden, açık kaynaklı ve kendi sunucunuzda barındırılan bir izleme aracı olan Checkmate'in hem frontend hem de backend tarafını içerir. Checkmate, bir sunucunun ya da web sitesinin erişilebilir ve düzgün çalışıyor olup olmadığını düzenli olarak kontrol eder; izlenen servislerin kullanılabilirliği, kesintileri ve yanıt süreleri hakkında gerçek zamanlı uyarılar ve raporlar sağlar.
Bu depo, sunucu donanımını, çalışma süresini, yanıt sürelerini ve olayları gerçek zamanlı olarak şık görselleştirmelerle takip eden, açık kaynaklı ve kendi sunucunuzda barındırabileceğiniz bir izleme aracı olan Checkmate'in hem frontend hem de backend tarafını içerir. Checkmate, bir sunucunun veya web sitesinin erişilebilir olup olmadığını ve düzgün çalışıp çalışmadığını düzenli olarak kontrol eder; izlenen servislerin erişilebilirliği, kesintileri ve yanıt süreleri hakkında gerçek zamanlı uyarılar ve raporlar sunar.
Checkmate'in ayrıca uzak sunuculardan veri çekmek için [Capture](https://github.com/bluewave-labs/capture) adında bir ajanı vardır. Capture, Checkmate'i çalıştırmak için zorunlu değildir; ancak sunucularınızın CPU, RAM, disk ve sıcaklık durumu hakkında ek bilgi sağlar. Capture; Linux, Windows, Mac, Raspberry Pi ve Go çalıştırabilen herhangi bir cihazda kullanılabilir.
Checkmate, uzak sunuculardan veri çekmek için [Capture](https://github.com/bluewave-labs/capture) adlı bir agent ile birlikte gelir. Capture, Checkmate'i çalıştırmak için zorunlu değildir; ancak sunucularınızın CPU, RAM, disk ve sıcaklık durumu hakkında ek bilgi sağlar. Capture; Linux, Windows, macOS, Raspberry Pi ve Go çalıştırabilen herhangi bir cihazda kullanılabilir.
Checkmate, 1000'den fazla aktif monitör ile herhangi bir sorun ya da performans darboğazı yaşamadan stres testinden geçirilmiştir.
Checkmate, 1000'den fazla aktif monitör ile fark edilir bir sorun veya performans darboğazı yaşamadan stres testinden geçirilmiştir.
## 📚 İçindekiler
@@ -63,9 +63,9 @@ Checkmate, 1000'den fazla aktif monitör ile herhangi bir sorun ya da performans
<a id="demo"></a>
## Demo
[Checkmate](https://demo.checkmate.so/)'in en son sürümünü iş başında görebilirsiniz.
[Checkmate](https://demo.checkmate.so/)'in en son sürümünü canlı olarak deneyebilirsiniz.
Kullanıcı adı demouser@demo.com, şifre Demouser1!'dir (Not: Demo sunucusunu zaman zaman güncelliyoruz; çalışmazsa Discussions kanalından bize haber verin).
Kullanıcı adı demouser@demo.com, şifre Demouser1!'dir. (Demo sunucusunu zaman zaman güncelliyoruz; çalışmıyorsa Discussions kanalından bize haber verin.)
<a id="users-guide"></a>
## Kullanıcı kılavuzu
@@ -81,7 +81,7 @@ Kullanım talimatlarını [burada](https://checkmate.so/docs) bulabilirsiniz.
Kurulum talimatları için [Checkmate dokümantasyon portalına](https://checkmate.so/docs) bakın.
Alternatif olarak hızlıca bir Checkmate örneği çalıştırmak için [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Güney Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) veya [Pikapods](https://www.pikapods.com/)'u kullanabilirsiniz. Sunucu altyapınızı izlemek isterseniz [Capture ajanına](https://github.com/bluewave-labs/capture) ihtiyacınız olacak. Capture deposu da kurulum talimatlarını içerir.
Bir Checkmate örneğini hızlıca ayağa kaldırmak için [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Güney Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) veya [Pikapods](https://www.pikapods.com/) da kullanabilirsiniz. Sunucu altyapınızı izlemek istiyorsanız [Capture agent'ına](https://github.com/bluewave-labs/capture) ihtiyacınız olacak; Capture deposunda da kurulum talimatları bulunur.
### Özel CA Kullanımı
@@ -92,7 +92,7 @@ Daha fazla dokümantasyon için [docs dizinine](../) göz atın.
<a id="performance"></a>
## Performans
Geniş kapsamlı optimizasyonlar sayesinde Checkmate son derece düşük bellek ayak iziyle çalışır ve minimum bellek ile CPU kaynağı gerektirir. Aşağıda her dakika 323 sunucuyu izleyen bir sunucudaki Node.js örneğinin bellek kullanımı yer almaktadır:
Yapılan geniş çaplı optimizasyonlar sayesinde Checkmate çok düşük bir bellek ayak iziyle çalışır ve çok az RAM ile CPU yeterlidir. Aşağıda, her dakika 323 sunucuyu izleyen bir sunucudaki Node.js örneğinin bellek kullanımı görülüyor:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
@@ -103,12 +103,12 @@ Aynı sayıda sunucu için aynı sunucudaki MongoDB ve Redis bellek ayak izini d
<a id="questions--ideas"></a>
## Sorular & fikirler
Sorularınız, önerileriniz ya da yorumlarınız varsa birkaç seçeneğiniz var:
Sorularınız, önerileriniz veya yorumlarınız varsa birkaç seçenek var:
- [Discord kanalı](https://discord.gg/NAb6H3UTjK) (tercih edilen)
- [Discord](https://discord.gg/NAb6H3UTjK) (tercih edilen)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (zaman zaman buraya bakıyoruz)
Soru sormaktan ya da fikirlerinizi paylaşmaktan çekinmeyin — sizi dinlemekten mutluluk duyarız!
Soru sormaktan ya da fikirlerinizi paylaşmaktan çekinmeyin — sizden haber almak güzel olur!
<a id="features"></a>
## Özellikler
+12 -12
View File
@@ -40,11 +40,11 @@
<br>
本仓库同时包含 Checkmate 的前端和后端代码。Checkmate 是一款开源、可自托管的监控工具,通过精美的可视化效果实时追踪服务器硬件、可用性、响应时间以及事件。Checkmate 会定期检查服务器或网站是否可访问、运行是否正常,并针对被监控服务的可用性、停机时间和响应时间提供实时告警与报告。
本仓库同时包含 Checkmate 的前端和后端代码。Checkmate 是一款开源、可自托管的监控工具,通过清晰的可视化实时追踪服务器硬件、可用性、响应时间以及事件。Checkmate 会定期检查服务器或网站是否可访问、运行是否正常,并针对被监控服务的可用性、停机时间和响应时间提供实时告警与报告。
Checkmate 还提供了一个名为 [Capture](https://github.com/bluewave-labs/capture) 的代理,用于从远程服务器拉取数据。Capture 并不是运行 Checkmate 所必需的,但它能提供服务器 CPU、内存、磁盘和温度的额外信息。Capture 可在 Linux、Windows、Mac、Raspberry Pi 或任何能运行 Go 的设备上运行。
Checkmate 还附带一个名为 [Capture](https://github.com/bluewave-labs/capture) 的代理,用于从远程服务器拉取数据。Capture 不是运行 Checkmate 的必需项,但能提供服务器 CPU、内存、磁盘和温度的额外信息。Capture 可在 Linux、Windows、macOS、Raspberry Pi 以及任何能运行 Go 的设备上运行。
Checkmate 已经在 1000+ 个活跃监控项的环境下完成压力测试,未出现任何明显问题或性能瓶颈。
Checkmate 已经在 1000+ 个活跃监控项的场景下完成了压力测试,未出现明显问题或性能瓶颈。
## 📚 目录
@@ -63,9 +63,9 @@ Checkmate 已经在 1000+ 个活跃监控项的环境下完成压力测试,未
<a id="demo"></a>
## 演示
你可以体验最新版本的 [Checkmate](https://demo.checkmate.so/)。
你可以直接体验最新版本的 [Checkmate](https://demo.checkmate.so/)。
用户名是 demouser@demo.com,密码是 Demouser1!(请注意,我们会不定期更新演示服务器,如果遇到问题,请在 Discussions 频道告诉我们)。
用户名是 demouser@demo.com,密码是 Demouser1!。(演示服务器会不定期更新;如果遇到无法访问的情况,请在 Discussions 频道告诉我们。)
<a id="users-guide"></a>
## 用户指南
@@ -81,7 +81,7 @@ Checkmate 已经在 1000+ 个活跃监控项的环境下完成压力测试,未
请参阅 [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 仓库中也提供了安装说明。
也可以通过 [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
@@ -92,29 +92,29 @@ Checkmate 已经在 1000+ 个活跃监控项的环境下完成压力测试,未
<a id="performance"></a>
## 性能
得益于大量优化,Checkmate 在运行时占用的内存非常少,对内存和 CPU 资源的需求极低。下方是一个每分钟监控 323 台服务器的 Node.js 实例的内存使用情况:
得益于大量优化,Checkmate 在运行时占用的内存非常少,对内存和 CPU 资源的需求极低。下面是一个每分钟监控 323 台服务器的 Node.js 实例的内存使用情况:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
同一台服务器、相同监控数量下的 MongoDB 与 Redis 的内存占用(分别为 398MB 与 15MB):
同一台服务器、相同监控数量下的 MongoDB 与 Redis 的内存占用(分别为 398MB 与 15MB):
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
<a id="questions--ideas"></a>
## 问题与想法
如果你有任何问题、建议或意见,欢迎通过以下方式联系我们:
如果你有任何问题、建议或意见,可以通过以下方式联系我们:
- [Discord 频道](https://discord.gg/NAb6H3UTjK)(推荐)
- [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、端口、游戏服务器
- 多种监控选项:可用性、Docker、Ping、SSL、端口、游戏服务器
- 页面速度监控
- 基础设施监控(内存、磁盘使用情况、CPU 性能、网络等) — 需要 [Capture](https://github.com/bluewave-labs/capture) 代理
- 通过挂载点选择实现的选择性磁盘监控
+34 -34
View File
@@ -40,11 +40,11 @@
<br>
本儲存庫同時包含 Checkmate 的前端與後端程式碼。Checkmate 是一款開源、可自我託管的監控工具,透過美觀的視覺化即時追蹤伺服器硬體、運作時間、回應時間以及事件。Checkmate 會定期檢查伺服器或網站是否可存取且運作正常,並針對被監控服務的可用性、停機時間和回應時間提供即時警報與報告。
本儲存庫同時包含 Checkmate 的前端與後端程式碼。Checkmate 是一款開源、可自我託管的監控工具,透過清楚的視覺化即時追蹤伺服器硬體、運作時間、回應時間以及事件。Checkmate 會定期檢查伺服器或網站是否可存取且運作正常,並針對被監控服務的可用性、停機時間與回應時間提供即時警報與報告。
Checkmate 還提供一個名為 [Capture](https://github.com/bluewave-labs/capture) 的代理程式,用以從遠端伺服器擷取資料。Capture 並非執行 Checkmate 的必要元件,但能提供伺服器 CPU、記憶體、磁碟與溫度的額外資訊。Capture 可在 Linux、Windows、Mac、Raspberry Pi 或任何能執行 Go 的裝置上運行。
Checkmate 還附帶一個名為 [Capture](https://github.com/bluewave-labs/capture) 的代理程式,用來從遠端伺服器擷取資料。Capture 並非執行 Checkmate 的必要元件,但能提供伺服器 CPU、記憶體、磁碟與溫度的額外資訊。Capture 可在 Linux、Windows、macOS、Raspberry Pi 或任何能執行 Go 的裝置上運行。
Checkmate 已在 1000+ 個活躍監控項目下進行過壓力測試,未發生任何明顯問題或效能瓶頸。
Checkmate 已在 1000+ 個活躍監控項目下完成壓力測試,未出現明顯問題或效能瓶頸。
## 📚 目錄
@@ -63,9 +63,9 @@ Checkmate 已在 1000+ 個活躍監控項目下進行過壓力測試,未發生
<a id="demo"></a>
## 展示
你可以體驗最新版本的 [Checkmate](https://demo.checkmate.so/)。
你可以直接體驗最新版本的 [Checkmate](https://demo.checkmate.so/)。
使用者名稱為 demouser@demo.com,密碼為 Demouser1!(我們會不定期更新示範伺服器,若無法使用,請於 Discussions 頻道告知我們)。
使用者名稱為 demouser@demo.com,密碼為 Demouser1!。(示範伺服器會不定期更新;若無法存取,請在 Discussions 頻道告知我們。)
<a id="users-guide"></a>
## 使用者指南
@@ -81,58 +81,58 @@ Checkmate 已在 1000+ 個活躍監控項目下進行過壓力測試,未發生
安裝說明請參閱 [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 儲存庫中也提供了安裝說明。
也可以透過 [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
如果你需要使用私有 CA(例如 Smallstep)發行的憑證來監控內部 HTTPS 端點,請參考我們的 [自訂 CA 信任指南](../custom-ca-trust.md),其中介紹了 Docker 的設定選項。
如果你需要使用私有 CA(例如 Smallstep)發行的憑證來監控內部 HTTPS 端點,請參考我們的 [自訂 CA 信任指南](../custom-ca-trust.md),其中介紹了 Docker 的設定選項。
更多文件請參閱 [docs 目錄](../)。
<a id="performance"></a>
## 效能
由於進行了大量最佳化,Checkmate 的記憶體佔用極小,所需記憶體與 CPU 資源也非常少。以下是每分鐘監控 323 台伺服器的 Node.js 實例的記憶體使用情況:
經過大量最佳化,Checkmate 的記憶體佔用極小,所需記憶體與 CPU 資源也非常少。以下是每分鐘監控 323 台伺服器的 Node.js 實例的記憶體使用情況:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
同一台伺服器、相同監控數量下,MongoDB 與 Redis 的記憶體佔用(分別為 398MB 與 15MB):
同一台伺服器、相同監控數量下,MongoDB 與 Redis 的記憶體佔用(分別為 398MB 與 15MB):
![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)(我們會不定期查看)
- [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、連接埠、遊戲伺服器
- 完全開源,可部署到自己的伺服器或家用裝置(例如 Raspberry Pi 4 / 5)
- 多種監控選項:可用性、Docker、Ping、SSL、連接埠、遊戲伺服器
- 頁面速度監控
- 基礎架構監控(記憶體、磁碟使用量、CPU 效能、網路等) — 需要 [Capture](https://github.com/bluewave-labs/capture) 代理程式
- 基礎架構監控(記憶體、磁碟使用量、CPU 效能、網路等) — 需要 [Capture](https://github.com/bluewave-labs/capture) 代理程式
- 透過掛載點選擇進行的選擇性磁碟監控
- 一目了然的事件檢視
- 提供 4 套精美主題的狀態頁
- 透過電子郵件、Webhook、Discord、Slack、PagerDuty、Matrix、Microsoft Teams、Telegram、Pushover、Twilio (SMS) 傳送通知
- 排程維護
- JSON 查詢監控
- 多語言支援:阿拉伯文、簡體中文、繁體中文(台灣)、捷克文、英文、芬蘭文、法文、德文、日文、葡萄牙文(巴西)、俄文、西班牙文、泰文、土耳其文、烏克蘭文與越南文
- 多語言支援:阿拉伯文、簡體中文、繁體中文(台灣)、捷克文、英文、芬蘭文、法文、德文、日文、葡萄牙文(巴西)、俄文、西班牙文、泰文、土耳其文、烏克蘭文與越南文
## 監控項目生命週期
1. 監控項目執行一次檢查(HTTP / Ping / 連接埠 / 透過 Capture 代理的硬體檢查)
2. 結果會被儲存(成功/失敗 + 回應時間)
1. 監控項目執行一次檢查(HTTP / Ping / 連接埠 / 透過 Capture 代理的硬體檢查)
2. 結果會被儲存(成功/失敗 + 回應時間)
3. 近期的檢查結果會依該監控項目所設定的狀態變更門檻進行評估
4. 當達到狀態變更門檻且目前狀態與上一次狀態不同時,監控項目的狀態會改變(例如 `initializing`、`up`、`down`、`breached`)
5. 狀態變更時,會根據監控項目目前的狀態建立或解決一個事件
4. 當達到狀態變更門檻且目前狀態與上一次狀態不同時,監控項目的狀態會改變(例如 `initializing`、`up`、`down`、`breached`)
5. 狀態變更時,會根據監控項目目前的狀態建立或解決一個事件
6. 通知會依照設定觸發
<a id="screenshots"></a>
@@ -157,38 +157,38 @@ Checkmate 已在 1000+ 個活躍監控項目下進行過壓力測試,未發生
## 技術堆疊
- [ReactJs](https://react.dev/)
- [MUI (React 框架)](https://mui.com/)
- [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/)。
- 如果你想支持我們,請考慮給專案點 ⭐ 並點選 "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),致力於協助個人與企業監控其基礎架構與伺服器。
我們是 [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+ 位貢獻者。
我們以與各層級貢獻者建立穩固關係為榮。儘管 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** 的員工點亮 — 所以請不要猶豫,加入我們,一起貢獻、一起學習吧!
我們的儲存庫已被來自 **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. 若你認為遇到了 bug,請開啟一個 issue。
4. 如果你是新手,建議從 `good-first-issue` 開始。
2. 如果想深入了解架構,請閱讀 [Checkmate 的詳細結構介紹](https://deepwiki.com/bluewave-labs/Checkmate)。
3. 若你認為遇到了 bug,請開啟一個 issue。
4. 如果你是新手,建議從 `good-first-issue` 開始。
5. 提交 pull request 來新增功能、改善體驗或修正 bug。
6. 透過 CodeCanvas 上的 `Checkmate` 程式碼互動導覽(請點 [這裡](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true))。若要精修現有的資料流模擬或建立新的模擬,請依 [此處](https://docs.code-canvas.com/updating-diagram) 的快速教學操作。
6. 試試 CodeCanvas 上 `Checkmate` 程式碼的互動導覽,請點 [這裡](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" />
-2
View File
@@ -11,8 +11,6 @@ LOG_LEVEL=debug
# Database
# --------
DB_CONNECTION_STRING=mongodb://localhost:27017/uptime_db
QUEUE_MODE="primary"
QUEUE_PRIMARY_PROCESSES="true"
# JWT Authentication
# ------------------
+1 -10
View File
@@ -12,14 +12,5 @@
"quoteProps": "as-needed",
"arrowParens": "always",
"trailingComma": "es5",
"htmlWhitespaceSensitivity": "css",
"plugins": ["prettier-plugin-ini"],
"overrides": [
{
"files": [".env", ".env.*"],
"options": {
"parser": "ini"
}
}
]
"htmlWhitespaceSensitivity": "css"
}
+1 -1
View File
@@ -39,7 +39,7 @@ export default [
},
},
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
},
+1
View File
@@ -8,6 +8,7 @@ const config: Config = {
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
},
moduleNameMapper: {
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.ts",
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
Loaded 100 of 417 files, more files were not shown because too many files have changed in this diff. Show more