Compare commits
226
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f92dc835b7 | ||
|
|
0b0d412187 | ||
|
|
77b817778b | ||
|
|
eddb9a82a8 | ||
|
|
9459edce00 | ||
|
|
eaf1064413 | ||
|
|
dae6f9a570 | ||
|
|
9fa45c5871 | ||
|
|
2810cd3562 | ||
|
|
f4177baed5 | ||
|
|
5feb666bdb | ||
|
|
8396f478c5 | ||
|
|
bbfd963deb | ||
|
|
fc0cda3da6 | ||
|
|
861d867a17 | ||
|
|
8d7c322279 | ||
|
|
68a9bece1e | ||
|
|
4f82a11c66 | ||
|
|
cc1814f507 | ||
|
|
436c11bcba | ||
|
|
d5ec2936ad | ||
|
|
937ffe4f3b | ||
|
|
0df71d6356 | ||
|
|
adba25269c | ||
|
|
386a384ef6 | ||
|
|
8bf5db0834 | ||
|
|
817d01c857 | ||
|
|
3f30acdbdf | ||
|
|
80b604b9ff | ||
|
|
9356b0d482 | ||
|
|
6699a83948 | ||
|
|
900c039672 | ||
|
|
a4ab7d662e | ||
|
|
807d7e10c3 | ||
|
|
39043f3945 | ||
|
|
5a72e1e123 | ||
|
|
959b150ebf | ||
|
|
552b7a037d | ||
|
|
8671685cf9 | ||
|
|
ff536f40a2 | ||
|
|
e3d63a1cb4 | ||
|
|
105d7f0580 | ||
|
|
e2a1f02971 | ||
|
|
b7f548ff15 | ||
|
|
29d05c1e57 | ||
|
|
0ec77a5ab1 | ||
|
|
95eb75f416 | ||
|
|
8f657dcd36 | ||
|
|
c0e33eb4fa | ||
|
|
754b0af3a3 | ||
|
|
a39792cf0b | ||
|
|
35c9618d59 | ||
|
|
c6c41c2e3e | ||
|
|
f58825d19f | ||
|
|
e4649f3d76 | ||
|
|
c7361666f5 | ||
|
|
3f5707721f | ||
|
|
cc5f033d0d | ||
|
|
d2d0fb788e | ||
|
|
9d969d67ca | ||
|
|
6e1c26b987 | ||
|
|
ef353dcb36 | ||
|
|
b21ef60cda | ||
|
|
f43bf9ab3d | ||
|
|
48ca64ec6f | ||
|
|
e036c6077e | ||
|
|
c9f0254aed | ||
|
|
b3d9c5b94c | ||
|
|
fd536541d7 | ||
|
|
7a82c76d5c | ||
|
|
5fab2d0a30 | ||
|
|
860b2d6214 | ||
|
|
8579edd56e | ||
|
|
969d24e369 | ||
|
|
52b95ad121 | ||
|
|
ea23610b3a | ||
|
|
a4cc322a55 | ||
|
|
2b9ff52f6b | ||
|
|
0f230c4d70 | ||
|
|
74783fd738 | ||
|
|
71d3e789ff | ||
|
|
f59a7c3113 | ||
|
|
ffbf223288 | ||
|
|
96021b7479 | ||
|
|
f0a5974eaf | ||
|
|
c9b8f434cd | ||
|
|
3685e0c476 | ||
|
|
d6f9fca907 | ||
|
|
ba943e90c2 | ||
|
|
dead97169f | ||
|
|
eb1d307932 | ||
|
|
a31dd2f19e | ||
|
|
d84fc7e299 | ||
|
|
8afd67a06d | ||
|
|
71a4fddffd | ||
|
|
3489ee000d | ||
|
|
93b9c5033a | ||
|
|
32ec909bf7 | ||
|
|
8e5757beb1 | ||
|
|
8e5a337f3e | ||
|
|
9db4e557b3 | ||
|
|
10174da4f0 | ||
|
|
99ded83bd5 | ||
|
|
dffa49ae9d | ||
|
|
89c86a7628 | ||
|
|
09446c9fc1 | ||
|
|
755134ee7f | ||
|
|
3b919044e6 | ||
|
|
08e9c02a3a | ||
|
|
85f674ee08 | ||
|
|
16036f421b | ||
|
|
7fe7b79103 | ||
|
|
a058c0518c | ||
|
|
5ed9ab780a | ||
|
|
e32590425b | ||
|
|
190a3b47da | ||
|
|
f740eef0fa | ||
|
|
5efd64d44d | ||
|
|
b28c84436e | ||
|
|
9082cf0826 | ||
|
|
9d453bdb0b | ||
|
|
39d92fb067 | ||
|
|
a54fdd89ab | ||
|
|
6aa9360b98 | ||
|
|
bb3f471d19 | ||
|
|
6d433fd0ac | ||
|
|
d8dafa311a | ||
|
|
398e5d7314 | ||
|
|
53ee45172d | ||
|
|
1ea6dafb1a | ||
|
|
9785dc146e | ||
|
|
1b1d8ea229 | ||
|
|
603bd1d814 | ||
|
|
158d976ccb | ||
|
|
380fdf470f | ||
|
|
1e2a4b4a84 | ||
|
|
5e0d8f0550 | ||
|
|
bffc4ee813 | ||
|
|
ad9ff7f5df | ||
|
|
8277c4ed13 | ||
|
|
f65238da6c | ||
|
|
64b8aab4d6 | ||
|
|
7244b35091 | ||
|
|
699883e0b1 | ||
|
|
f7211711ce | ||
|
|
cb0dd878d6 | ||
|
|
8c5be9ef00 | ||
|
|
d1b6d08c73 | ||
|
|
2d68e96782 | ||
|
|
306f2a60a2 | ||
|
|
abf6dc2908 | ||
|
|
7ce6068757 | ||
|
|
a7d9bb822e | ||
|
|
142ecaaf8b | ||
|
|
90bbc764fc | ||
|
|
ae4c433246 | ||
|
|
d2461ef118 | ||
|
|
47162f2064 | ||
|
|
8b9c41464a | ||
|
|
51b6ec1751 | ||
|
|
6165ae60e6 | ||
|
|
69e142f711 | ||
|
|
eff4f8ad69 | ||
|
|
bca902471d | ||
|
|
1b3f2f0365 | ||
|
|
be12991997 | ||
|
|
54596fe776 | ||
|
|
8d637dbcac | ||
|
|
771fd5b50f | ||
|
|
8dedf2a742 | ||
|
|
c882daf000 | ||
|
|
1aa7ae8f11 | ||
|
|
517139cbf4 | ||
|
|
ef049b6521 | ||
|
|
33f8641f3a | ||
|
|
2f22a52116 | ||
|
|
8af13b04ec | ||
|
|
ac77216f07 | ||
|
|
a43363ebfc | ||
|
|
0ca738b080 | ||
|
|
091c36cbf3 | ||
|
|
120708ed47 | ||
|
|
6a1ab622b3 | ||
|
|
0aef8b652f | ||
|
|
20863fbce7 | ||
|
|
fa7eaa4788 | ||
|
|
ee9c08b1bb | ||
|
|
7ee30e12f5 | ||
|
|
c5f57eda62 | ||
|
|
2bf3c08f3c | ||
|
|
187c8eaba1 | ||
|
|
25d8839441 | ||
|
|
0f4240d3db | ||
|
|
a37bc63f31 | ||
|
|
73f95ac8e1 | ||
|
|
c8c4bac2c7 | ||
|
|
9fecfa562f | ||
|
|
7991f3f6a1 | ||
|
|
692d4d89d6 | ||
|
|
f1b104b78d | ||
|
|
3c2b571313 | ||
|
|
abec4e6c70 | ||
|
|
d022f7d849 | ||
|
|
0fb210e9e1 | ||
|
|
ce4cb63035 | ||
|
|
f637b01631 | ||
|
|
e390bd764f | ||
|
|
30e8cfc9ec | ||
|
|
80d6868a1d | ||
|
|
942d75afcd | ||
|
|
c34fcc5cb3 | ||
|
|
409f9f6dc6 | ||
|
|
525206c1c1 | ||
|
|
9a85389b8c | ||
|
|
eeac784922 | ||
|
|
19b873ca87 | ||
|
|
0f99c7004d | ||
|
|
6a566c3693 | ||
|
|
dfcf0309fe | ||
|
|
395b7fbe2a | ||
|
|
d2c13fac5a | ||
|
|
dd16ac060a | ||
|
|
5628c202f7 | ||
|
|
372fc9869b | ||
|
|
87a6640c1d | ||
|
|
4e31499f78 |
No files matched your search
@@ -68,19 +68,23 @@ VITE_APP_LOG_LEVEL="debug"
|
||||
### Backend Layers
|
||||
```
|
||||
server/src/
|
||||
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
├── service/ # Business logic
|
||||
│ ├── business/ # Core monitoring logic
|
||||
│ ├── infrastructure/ # Server/system utilities
|
||||
│ └── system/ # App-level settings
|
||||
├── api/ # HTTP layer
|
||||
│ ├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
│ ├── middleware/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
│ ├── routes/ # API route definitions
|
||||
│ └── validation/ # Zod request-payload schemas
|
||||
├── domain/ # Business logic + data access, one folder per entity
|
||||
│ ├── monitors/ # e.g. monitor.service.ts, monitor.repository.ts, monitor.type.ts
|
||||
│ ├── checks/
|
||||
│ ├── incidents/
|
||||
│ └── … # users, teams, status-pages, notifications, etc.
|
||||
├── service/ # Cross-cutting services (e.g. infrastructure/)
|
||||
├── config/ # App config wiring (controllers, routes, services, envValidation)
|
||||
├── db/
|
||||
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
||||
│ ├── migration/ # Database migrations (run on startup)
|
||||
│ └── modules/ # Database-specific modules
|
||||
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
├── routes/v1/ # API route definitions
|
||||
├── validation/ # Joi input validation schemas
|
||||
└── repositories/ # Data access layer
|
||||
│ └── migration/ # Database migrations (run on startup)
|
||||
├── templates/ # Email/notification templates
|
||||
├── types/ # Shared const tuples + derived types
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
### Frontend Structure
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="docs/translations/README.es.md">Español</a> |
|
||||
🇫🇮 <a href="docs/translations/README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="docs/translations/README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="docs/translations/README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="docs/translations/README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="docs/translations/README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="docs/translations/README.ru.md">Русский</a> |
|
||||
|
||||
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: checkmate-chart
|
||||
description: A Helm chart for Checkmate App
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "2.3"
|
||||
version: 0.2.0
|
||||
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
|
||||
@@ -19,12 +19,11 @@ cd checkmate/charts/helm/checkmate
|
||||
|
||||
### 2. Customize values.yaml
|
||||
Edit `values.yaml` to update:
|
||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||
- `server.protocol` (usually http or https)
|
||||
- `client.ingress.host` and `api.ingress.host` with your domain names
|
||||
- `api.protocol` (usually http or https)
|
||||
- **If upgrading**: Migrate persistence settings from flat structure to nested:
|
||||
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
|
||||
- Old: `persistence.redisSize` → New: `persistence.redis.size`
|
||||
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
|
||||
- Add: `persistence.mongo.storageClass` (leave empty for default)
|
||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
|
||||
|
||||
@@ -32,7 +31,7 @@ Edit `values.yaml` to update:
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate
|
||||
```
|
||||
This will deploy the client, server, MongoDB, and Redis components.
|
||||
This will deploy the client, api, and MongoDB components.
|
||||
|
||||
### 4. Verify the deployment
|
||||
Check pods and services:
|
||||
@@ -43,6 +42,137 @@ kubectl get svc
|
||||
|
||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||
|
||||
## Upgrading from a previous release
|
||||
|
||||
This release renames the `server.*` value block to `api.*` and drops the (unused) bundled Redis.
|
||||
Read this before running `helm upgrade` on an existing install.
|
||||
|
||||
### `server.*` → `api.*` (values rename)
|
||||
|
||||
The value block formerly named `server` is now `api` (`server.image` → `api.image`,
|
||||
`server.ingress.host` → `api.ingress.host`, `server.protocol` → `api.protocol`, `server.resources`,
|
||||
`server.affinity`, `server.tolerations`, `server.ingress.*`, etc.).
|
||||
|
||||
**Your existing `server.*` overrides keep working** — the chart merges any legacy `server.*` values on
|
||||
top of the `api.*` defaults (legacy wins where set), so an unchanged values file upgrades cleanly. The
|
||||
shim is transitional; migrate your overrides to `api.*` at your convenience and drop the `server:`
|
||||
block. If you set **both** `server.*` and `api.*` for the same field, the legacy `server.*` value wins —
|
||||
don't mix them.
|
||||
|
||||
### Resource renames cause a replace, not a rename
|
||||
|
||||
The Kubernetes objects are renamed alongside the values:
|
||||
|
||||
| Old object | New object |
|
||||
| --- | --- |
|
||||
| Deployment/Service `checkmate-server` | `checkmate-api` |
|
||||
| Ingress `checkmate-server-ingress` | `checkmate-api-ingress` |
|
||||
| ConfigMap `checkmate-server-nginx-cm` | `checkmate-client-nginx-cm` |
|
||||
|
||||
Helm deletes the old objects and creates the new ones on upgrade. Expect a brief API restart and a
|
||||
**new ClusterIP** for the API Service — update anything outside the chart that referenced the
|
||||
`checkmate-server` Service by name. MongoDB data (its PVC) is untouched.
|
||||
|
||||
### Redis removed
|
||||
|
||||
The bundled Redis StatefulSet/Service and the `redis.*` / `persistence.redis.*` values are gone — the
|
||||
scheduler runs in-process and never used them. Default installs (`redis.enabled: false`) are
|
||||
unaffected. If you had explicitly set `redis.enabled: true`, **the Redis StatefulSet and its PVC are
|
||||
deleted on upgrade** — back up anything you stored there first. Leftover `redis:` / `persistence.redis:`
|
||||
keys in your values file are harmless (ignored).
|
||||
|
||||
### New secret defaults
|
||||
|
||||
`NODE_ENV: production`, `LOG_LEVEL: info`, and `TOKEN_TTL: 99d` are now part of the default `secrets`
|
||||
block and merge into your install unless you override them. Set them explicitly if you relied on
|
||||
different values (e.g. a non-production `NODE_ENV`).
|
||||
|
||||
### Image tags follow the chart's `appVersion`
|
||||
|
||||
Each tier's `image` value is now the repository **without a tag**; the tag defaults to the chart's
|
||||
`appVersion`, so the client, api, worker, and mongodb images all move together when you upgrade the
|
||||
chart. You can still pin per tier:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend # repo only
|
||||
tag: "" # empty → Chart.appVersion; set e.g. "v3.1.0" to pin, or "latest" for a dev tag
|
||||
```
|
||||
|
||||
- `<tier>.tag` overrides the version for that tier. The `worker` tier inherits `api.tag` (and
|
||||
`api.image`) unless you set `worker.tag` / `worker.image`.
|
||||
- If you put a **full `repo:tag`** in `image` (e.g. a legacy `server.image: …:v3.2.0` override), it is
|
||||
used verbatim and the `tag` field is ignored — so existing pinned values keep working unchanged.
|
||||
|
||||
**Prefer a pinned tag over `latest`.** With the default `imagePullPolicy: IfNotPresent`, `latest` is
|
||||
cached per node and never refreshed, and `helm rollback` can't move a floating tag — pin a version (or
|
||||
let it default to `appVersion`) for reproducible, rollback-able deploys.
|
||||
|
||||
## Scaling: the worker tier & autoscaling
|
||||
|
||||
By default the chart runs a single all-in-one API pod that both schedules **and** processes monitoring
|
||||
jobs (`worker.enabled: false`). For larger deployments you can split processing onto a dedicated,
|
||||
horizontally-scalable worker tier.
|
||||
|
||||
### Topology
|
||||
|
||||
| `worker.enabled` | API (`checkmate-api`) | Worker (`checkmate-worker`) |
|
||||
| --- | --- | --- |
|
||||
| `false` (default) | schedules **and** processes jobs (`QUEUE_PRIMARY_PROCESSES=true`) | not deployed |
|
||||
| `true` | schedules + serves the API only (`QUEUE_PRIMARY_PROCESSES=false`, derived) | processes all jobs |
|
||||
|
||||
`QUEUE_PRIMARY_PROCESSES` is **derived** from `worker.enabled` — you never set it directly, so the API
|
||||
and worker tier can never both process (or both ignore) the queue.
|
||||
|
||||
### Enabling the worker tier
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
enabled: true
|
||||
replicas: 2 # used only when autoscaling.enabled = false
|
||||
```
|
||||
|
||||
The worker takes **no inbound traffic** (no Service): the kubelet probes pod IPs directly on
|
||||
`HEALTH_PORT` (`/livez`, `/readyz`), and graceful shutdown drains in-flight jobs before exit.
|
||||
|
||||
> **Invariant:** `worker.terminationGracePeriodSeconds` (default `30`) **must stay greater than the
|
||||
> server drain timeout (25s)**. Lower it and Kubernetes SIGKILLs workers mid-drain, losing in-flight
|
||||
> checks.
|
||||
|
||||
> **Caveat:** with the worker tier enabled the API no longer processes jobs, so if **all** workers are
|
||||
> down nothing processes. The `autoscaling.minReplicaCount: 1` floor plus liveness restarts mitigate
|
||||
> this; full failover to the API would need leader election (not supported).
|
||||
|
||||
### Autoscaling with KEDA
|
||||
|
||||
Backlog-driven autoscaling is **off by default** and requires the
|
||||
[KEDA operator](https://keda.sh/) installed in-cluster. Enable it with:
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
enabled: true
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicaCount: 1 # processing floor — never 0, or checks silently stall
|
||||
maxReplicaCount: 10
|
||||
backlogPerReplica: 50 # target due-job backlog per worker
|
||||
mongo:
|
||||
dbName: uptime_db # MUST match the database in DB_CONNECTION_STRING
|
||||
```
|
||||
|
||||
A KEDA `ScaledObject` queries MongoDB directly for the due-check backlog and scales the worker
|
||||
Deployment between `minReplicaCount` and `maxReplicaCount`. When autoscaling is on, the Deployment
|
||||
omits `replicas` so it doesn't fight the HPA KEDA creates. Make sure `autoscaling.mongo.dbName`
|
||||
matches the database name in your `DB_CONNECTION_STRING`, or the scaler counts the wrong DB (always 0
|
||||
backlog → never scales up).
|
||||
|
||||
#### Fallback: Prometheus-adapter HPA (not shipped)
|
||||
|
||||
If your KEDA/Mongo versions reject the `$$NOW`/`$toLong` query, or you already run Prometheus, scrape
|
||||
the worker's `/metrics` (`checkmate_worker_due_backlog`), expose it via prometheus-adapter, and target
|
||||
it with a standard `HorizontalPodAutoscaler`. The manifest is left to the operator; the app computes
|
||||
`now` in code on this path, so the query coercion is unnecessary.
|
||||
|
||||
## Enabling TLS/HTTPS with cert-manager
|
||||
|
||||
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
|
||||
@@ -68,7 +198,7 @@ client:
|
||||
enabled: true
|
||||
secretName: checkmate-client-tls
|
||||
|
||||
server:
|
||||
api:
|
||||
protocol: https
|
||||
ingress:
|
||||
enabled: true
|
||||
@@ -78,7 +208,7 @@ server:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: checkmate-server-tls
|
||||
secretName: checkmate-api-tls
|
||||
```
|
||||
|
||||
### Alternative: Using --set flags
|
||||
@@ -88,13 +218,13 @@ You can also enable TLS during installation using Helm's `--set` flags:
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate \
|
||||
--set client.protocol=https \
|
||||
--set server.protocol=https \
|
||||
--set api.protocol=https \
|
||||
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set client.ingress.tls.enabled=true \
|
||||
--set client.ingress.tls.secretName=checkmate-client-tls \
|
||||
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set server.ingress.tls.enabled=true \
|
||||
--set server.ingress.tls.secretName=checkmate-server-tls
|
||||
--set api.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set api.ingress.tls.enabled=true \
|
||||
--set api.ingress.tls.secretName=checkmate-api-tls
|
||||
```
|
||||
|
||||
### Verification
|
||||
@@ -107,7 +237,7 @@ kubectl get certificates
|
||||
|
||||
# Check certificate details
|
||||
kubectl describe certificate checkmate-client-tls
|
||||
kubectl describe certificate checkmate-server-tls
|
||||
kubectl describe certificate checkmate-api-tls
|
||||
|
||||
# Verify the secrets were created
|
||||
kubectl get secrets | grep checkmate-tls
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{{- /*
|
||||
Back-compat shim: the `server.*` value block was renamed to `api.*`. Existing installs that
|
||||
still pass a legacy `server:` block keep working — this returns the effective API values with any
|
||||
legacy `server.*` overrides merged on top of the `api.*` defaults (legacy wins where set).
|
||||
|
||||
New installs have no `server:` key, so this is a no-op deepCopy of `.Values.api`. Consume it as:
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
then reference `$api.*` instead of `.Values.api.*`.
|
||||
*/}}
|
||||
{{- define "checkmate.api" -}}
|
||||
{{- $api := deepCopy .Values.api -}}
|
||||
{{- with .Values.server -}}
|
||||
{{- $api = mergeOverwrite $api (deepCopy .) -}}
|
||||
{{- end -}}
|
||||
{{- $api | toYaml -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Resolve a full image reference from a repo + optional tag. `appVersion` is the single source of
|
||||
truth for tags: bumping Chart.appVersion moves every tier at once. Call as:
|
||||
{{ include "checkmate.image" (dict "image" $repo "tag" $tag "root" $) }}
|
||||
|
||||
If `image` already carries a tag (a ":" in its final path segment) it is used verbatim — this keeps
|
||||
legacy `server.image: repo:tag` overrides working and lets anyone pin a full ref (e.g. tag "latest").
|
||||
A ":" only in the registry segment (a registry port like "reg:5000/x") is ignored, so a tag is still
|
||||
appended. Otherwise the tag is `tag` if set, else the chart's appVersion.
|
||||
*/}}
|
||||
{{- define "checkmate.image" -}}
|
||||
{{- $lastSegment := .image | splitList "/" | last -}}
|
||||
{{- if contains ":" $lastSegment -}}
|
||||
{{- .image -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" .image (.tag | default .root.Chart.AppVersion) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,65 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-api
|
||||
spec:
|
||||
replicas: 1 # single scheduler — seeds/reconciles the jobs collection
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-api
|
||||
spec:
|
||||
{{- with $api.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $api.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: api
|
||||
image: {{ include "checkmate.image" (dict "image" $api.image "tag" $api.tag "root" $) }}
|
||||
imagePullPolicy: {{ $api.imagePullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $api.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
# Probes hit the Express /api/v1/health endpoint — proves the HTTP server is responsive.
|
||||
# NOTE: this does NOT detect a wedged job loop when queuePrimaryProcesses=true (the API is
|
||||
# also a worker). Worker-aware health for that mode is a follow-up — see worker-health-checks.md §7.5.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: http
|
||||
initialDelaySeconds: {{ $api.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $api.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $api.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $api.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: http
|
||||
initialDelaySeconds: {{ $api.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $api.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $api.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $api.probes.readiness.failureThreshold }}
|
||||
{{- with $api.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
- name: QUEUE_MODE
|
||||
value: "primary"
|
||||
# Derived: when a worker tier is deployed the API only schedules (false); otherwise it is
|
||||
# all-in-one and processes jobs itself (true). Keeps the two from ever competing for jobs.
|
||||
- name: QUEUE_PRIMARY_PROCESSES
|
||||
value: {{ not .Values.worker.enabled | quote }}
|
||||
+12
-11
@@ -1,11 +1,12 @@
|
||||
{{- if .Values.server.ingress.enabled }}
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if $api.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: checkmate-server-ingress
|
||||
{{- if .Values.server.ingress.annotations }}
|
||||
name: checkmate-api-ingress
|
||||
{{- if $api.ingress.annotations }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.server.ingress.annotations }}
|
||||
{{- range $key, $value := $api.ingress.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -17,22 +18,22 @@ metadata:
|
||||
#nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
|
||||
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.server.ingress.className }}
|
||||
{{- if .Values.server.ingress.tls.enabled }}
|
||||
ingressClassName: {{ $api.ingress.className }}
|
||||
{{- if $api.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.server.ingress.host }}
|
||||
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
|
||||
- {{ $api.ingress.host }}
|
||||
secretName: {{ default (printf "%s-api-tls" .Release.Name) $api.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
- host: {{ $api.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /api/v1
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: checkmate-server
|
||||
name: checkmate-api
|
||||
port:
|
||||
number: {{ .Values.server.port }}
|
||||
number: {{ $api.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-api
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-api
|
||||
ports:
|
||||
- port: {{ $api.port }}
|
||||
+5
-3
@@ -1,3 +1,4 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -22,12 +23,13 @@ spec:
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: client
|
||||
image: {{ .Values.client.image }}
|
||||
image: {{ include "checkmate.image" (dict "image" .Values.client.image "tag" .Values.client.tag "root" $) }}
|
||||
imagePullPolicy: {{ .Values.client.imagePullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.client.port }}
|
||||
env:
|
||||
- name: UPTIME_APP_API_BASE_URL
|
||||
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
|
||||
value: "{{ $api.protocol }}://{{ $api.ingress.host }}/api/v1"
|
||||
- name: UPTIME_APP_CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
volumeMounts:
|
||||
@@ -41,4 +43,4 @@ spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: checkmate-server-nginx-cm
|
||||
name: checkmate-client-nginx-cm
|
||||
File renamed without changes.
+5
-4
@@ -1,7 +1,8 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: checkmate-server-nginx-cm
|
||||
name: checkmate-client-nginx-cm
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
@@ -22,7 +23,7 @@ data:
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://{{ .Values.server.ingress.host }}:5000/api/;
|
||||
# proxy_pass http://{{ $api.ingress.host }}:5000/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -31,7 +32,7 @@ data:
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://{{ .Values.server.ingress.host }}:5000/api-docs/;
|
||||
proxy_pass http://{{ $api.ingress.host }}:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
File renamed without changes.
File renamed without changes.
+5
-2
@@ -22,7 +22,8 @@ spec:
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: {{ .Values.mongodb.image }}
|
||||
image: {{ include "checkmate.image" (dict "image" .Values.mongodb.image "tag" .Values.mongodb.tag "root" $) }}
|
||||
imagePullPolicy: {{ .Values.mongodb.imagePullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mongodb.port }}
|
||||
command: ["mongod", "--quiet", "--bind_ip_all"]
|
||||
@@ -37,7 +38,9 @@ spec:
|
||||
- metadata:
|
||||
name: checkmate-mongo-persistent-storage
|
||||
spec:
|
||||
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
|
||||
{{- with .Values.persistence.mongo.storageClass }}
|
||||
storageClassName: {{ . | quote }} {{/* omitted when empty so the cluster's default StorageClass applies */}}
|
||||
{{- end }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
@@ -1,14 +1,15 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- if eq $api.ingress.host "change_me" }}
|
||||
{{- fail "api.ingress.host (or legacy server.ingress.host) must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $serverProtocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- $apiProtocol := $api.protocol }}
|
||||
{{- if not (or (eq $apiProtocol "http") (eq $apiProtocol "https")) }}
|
||||
{{- fail "api.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $clientProtocol := .Values.client.protocol }}
|
||||
@@ -21,28 +22,33 @@
|
||||
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
|
||||
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
|
||||
{{- if and $api.ingress.tls.enabled (ne $apiProtocol "https") }}
|
||||
{{- fail "api.ingress.tls.enabled is true but api.protocol is not 'https'. Set api.protocol: https to ensure correct API base URL." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* If client runs on https, API must also be https to avoid mixed content */}}
|
||||
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
|
||||
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
|
||||
{{- if and (eq $clientProtocol "https") (ne $apiProtocol "https") }}
|
||||
{{- fail "client.protocol is 'https' but api.protocol is not. Set api.protocol: https to prevent browser mixed-content issues." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
|
||||
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
|
||||
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
|
||||
{{- $aAnn := $api.ingress.annotations | default dict }}
|
||||
|
||||
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
|
||||
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
|
||||
{{- $apiHasIssuer := or (hasKey $aAnn "cert-manager.io/cluster-issuer") (hasKey $aAnn "cert-manager.io/issuer") }}
|
||||
|
||||
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
|
||||
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
|
||||
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- if and $api.ingress.tls.enabled (not $apiHasIssuer) }}
|
||||
{{- fail "api.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
|
||||
{{/* Worker grace period must exceed the server drain timeout (25s) or workers are SIGKILLed mid-drain */}}
|
||||
{{- if and .Values.worker.enabled (le (int .Values.worker.terminationGracePeriodSeconds) 25) }}
|
||||
{{- fail "worker.terminationGracePeriodSeconds must be greater than 25 (the server drain timeout) or workers are killed mid-drain" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Secret name can be omitted; we default to <release>-client|api-tls in templates */}}
|
||||
@@ -1,11 +0,0 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-redis
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-redis
|
||||
ports:
|
||||
- port: {{ .Values.redis.port }}
|
||||
{{- end }}
|
||||
@@ -1,33 +0,0 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: checkmate-redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .Values.redis.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.redis.port }}
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: checkmate-redis-persistent-storage
|
||||
spec:
|
||||
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
|
||||
{{- end }}
|
||||
@@ -4,6 +4,14 @@
|
||||
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- /*
|
||||
The bundled Mongo service ("checkmate-mongodb") lives in .Release.Namespace. App pods reach it
|
||||
by bare name, but the KEDA operator runs in its own namespace and a bare name is NXDOMAIN there,
|
||||
so worker autoscaling fails to query the backlog. Qualify the in-cluster host to its FQDN here so
|
||||
the one connection string resolves from every namespace. External/already-qualified hosts are not
|
||||
matched (the char after the host name is "." for an FQDN, which the pattern excludes).
|
||||
*/}}
|
||||
{{- $mongoFqdn := printf "checkmate-mongodb.%s.svc.cluster.local" .Release.Namespace }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -11,5 +19,9 @@ metadata:
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := $secrets }}
|
||||
{{- if eq $key "DB_CONNECTION_STRING" }}
|
||||
{{ $key }}: {{ regexReplaceAll "(//|@)checkmate-mongodb([:/,]|$)" $value (printf "${1}%s${2}" $mongoFqdn) | quote }}
|
||||
{{- else }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,37 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-server
|
||||
spec:
|
||||
{{- with .Values.server.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
image: {{ .Values.server.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.server.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
{{- with .Values.server.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-server
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-server
|
||||
ports:
|
||||
- port: {{ .Values.server.port }}
|
||||
@@ -0,0 +1,66 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if .Values.worker.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-worker
|
||||
spec:
|
||||
{{- if not .Values.worker.autoscaling.enabled }}
|
||||
replicas: {{ .Values.worker.replicas }} # omitted when autoscaling owns the replica count
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-worker
|
||||
spec:
|
||||
terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }}
|
||||
{{- with .Values.worker.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.worker.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: worker
|
||||
image: {{ include "checkmate.image" (dict "image" (.Values.worker.image | default $api.image) "tag" (.Values.worker.tag | default $api.tag) "root" $) }}
|
||||
imagePullPolicy: {{ .Values.worker.imagePullPolicy | default $api.imagePullPolicy }}
|
||||
ports:
|
||||
- name: health
|
||||
containerPort: {{ .Values.worker.healthPort }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets # DB_CONNECTION_STRING (+ JWT_SECRET/CLIENT_HOST for validateEnv)
|
||||
env:
|
||||
# Secret default CLIENT_HOST is the non-URL "change_me"; override like the API so validateEnv passes.
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
- name: QUEUE_MODE
|
||||
value: "worker"
|
||||
- name: HEALTH_PORT
|
||||
value: {{ .Values.worker.healthPort | quote }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /livez
|
||||
port: health
|
||||
initialDelaySeconds: {{ .Values.worker.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.worker.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.worker.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: health
|
||||
initialDelaySeconds: {{ .Values.worker.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.worker.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.worker.probes.readiness.failureThreshold }}
|
||||
{{- with .Values.worker.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- if and .Values.worker.enabled .Values.worker.autoscaling.enabled }}
|
||||
apiVersion: keda.sh/v1alpha1
|
||||
kind: ScaledObject
|
||||
metadata:
|
||||
name: checkmate-worker
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
name: checkmate-worker
|
||||
minReplicaCount: {{ .Values.worker.autoscaling.minReplicaCount }} # floor: never 0 — a cold gap stops checks
|
||||
maxReplicaCount: {{ .Values.worker.autoscaling.maxReplicaCount }}
|
||||
cooldownPeriod: {{ .Values.worker.autoscaling.cooldownPeriod }}
|
||||
advanced:
|
||||
horizontalPodAutoscalerConfig:
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: {{ .Values.worker.autoscaling.stabilizationWindowSeconds }}
|
||||
triggers:
|
||||
- type: mongodb
|
||||
metadata:
|
||||
dbName: {{ .Values.worker.autoscaling.mongo.dbName | quote }}
|
||||
collection: {{ .Values.worker.autoscaling.mongo.collection | quote }}
|
||||
# The Phase 2 countDueBacklog predicate, time-relativized with $$NOW (epoch-ms via $toLong):
|
||||
query: >-
|
||||
{ "type": "check", "isActive": true,
|
||||
"$expr": { "$lte": ["$nextScheduledAt", { "$toLong": "$$NOW" }] },
|
||||
"$or": [ { "lockedUntil": null },
|
||||
{ "$expr": { "$lt": ["$lockedUntil", { "$toLong": "$$NOW" }] } } ] }
|
||||
queryValue: {{ .Values.worker.autoscaling.backlogPerReplica | quote }}
|
||||
connectionStringFromEnv: DB_CONNECTION_STRING # resolved from the worker's checkmate-secrets
|
||||
{{- end }}
|
||||
@@ -1,5 +1,7 @@
|
||||
client:
|
||||
image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0
|
||||
image: ghcr.io/bluewave-labs/checkmate-client
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 80
|
||||
protocol: http
|
||||
ingress:
|
||||
@@ -17,10 +19,24 @@ client:
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
|
||||
api:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 52345
|
||||
protocol: http
|
||||
# Liveness/readiness probes hit GET /api/v1/health on the api port (proves the HTTP server is up).
|
||||
probes:
|
||||
liveness:
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
@@ -31,40 +47,62 @@ server:
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: false
|
||||
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
|
||||
# Note: when enabling TLS, also set server.protocol: https and add
|
||||
# secretName: {{ .Release.Name }}-api-tls # Optional; defaults to <release>-api-tls if omitted
|
||||
# Note: when enabling TLS, also set api.protocol: https and add
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
image: redis:7.2
|
||||
port: 6379
|
||||
|
||||
mongodb:
|
||||
image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0
|
||||
image: ghcr.io/bluewave-labs/checkmate-mongo
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 27017
|
||||
|
||||
worker:
|
||||
# Off by default; existing installs are unchanged. When true, a dedicated worker processes all
|
||||
# jobs and the API stops processing (QUEUE_PRIMARY_PROCESSES is derived as false). When false, the
|
||||
# API is all-in-one and processes jobs itself.
|
||||
enabled: false
|
||||
image: "" # defaults to api.image when empty, same as API image
|
||||
tag: "" # defaults to api.tag (-> Chart.appVersion) when empty
|
||||
imagePullPolicy: "" # defaults to api.imagePullPolicy when empty
|
||||
replicas: 1 # used only when autoscaling.enabled = false
|
||||
healthPort: 52346 # HEALTH_PORT; serves /livez /readyz /metrics
|
||||
terminationGracePeriodSeconds: 30 # MUST stay > the server drain timeout (25s) or workers are killed mid-drain
|
||||
resources: {}
|
||||
affinity: {}
|
||||
tolerations: {}
|
||||
probes:
|
||||
liveness: # /livez — failureThreshold>=3 so one slow tick never restarts mid-job
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness: # /readyz — flips to 503 on drain
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
autoscaling:
|
||||
enabled: false # requires the KEDA operator installed in-cluster
|
||||
minReplicaCount: 1 # processing floor — never 0 (a cold gap silently stops checks)
|
||||
maxReplicaCount: 10
|
||||
backlogPerReplica: 50 # KEDA queryValue: target due-backlog per worker
|
||||
cooldownPeriod: 300 # seconds before scaling an idle target down
|
||||
stabilizationWindowSeconds: 300 # damp the interval-boundary herd
|
||||
mongo:
|
||||
dbName: uptime_db # must match DB_CONNECTION_STRING's database
|
||||
collection: jobs
|
||||
|
||||
secrets:
|
||||
JWT_SECRET: change_me
|
||||
# REFRESH_TOKEN_SECRET: change_me
|
||||
# SYSTEM_EMAIL_ADDRESS: test@example.com
|
||||
# SYSTEM_EMAIL_PASSWORD: change_me
|
||||
# SYSTEM_EMAIL_HOST: smtp.example.com
|
||||
# SYSTEM_EMAIL_PORT: "587"
|
||||
# PAGESPEED_API_KEY: change_me
|
||||
DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db
|
||||
DB_CONNECTION_STRING: mongodb://checkmate-mongodb:27017/uptime_db
|
||||
CLIENT_HOST: change_me
|
||||
# REDIS_HOST: redis
|
||||
# REDIS_PORT: "6379"
|
||||
# DB_TYPE: MongoDB
|
||||
# TOKEN_TTL: 99d
|
||||
# REFRESH_TOKEN_TTL: 99d
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
TOKEN_TTL: 99d
|
||||
|
||||
persistence:
|
||||
mongo:
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
redis:
|
||||
size: 1Gi
|
||||
storageClass: ""
|
||||
Generated
+319
-189
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "3.8.1",
|
||||
"version": "3.9.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "client",
|
||||
"version": "3.8.1",
|
||||
"version": "3.9.2",
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
@@ -23,7 +24,7 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"joi": "17.13.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^5.19.0",
|
||||
"mui-color-input": "^7.0.0",
|
||||
@@ -64,10 +65,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -76,26 +79,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.28.6",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -116,11 +123,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -130,11 +139,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.28.6",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -144,30 +155,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.28.6",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.28.6",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.28.6",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -185,42 +202,52 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.6",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -265,27 +292,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -293,16 +324,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"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",
|
||||
@@ -1619,7 +1671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"version": "1.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
||||
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1651,9 +1705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1664,9 +1718,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1677,9 +1731,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1690,9 +1744,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1703,9 +1757,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1716,9 +1770,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1729,9 +1783,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1742,9 +1796,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1755,9 +1809,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1768,9 +1822,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1781,9 +1835,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1794,9 +1848,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1807,9 +1861,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1820,9 +1874,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1833,9 +1887,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1846,9 +1900,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1859,9 +1913,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1872,9 +1926,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1885,9 +1939,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1898,9 +1952,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1911,9 +1965,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1924,9 +1978,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1937,9 +1991,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1950,9 +2004,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1963,9 +2017,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2298,7 +2352,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
@@ -2724,6 +2780,18 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
@@ -2962,13 +3030,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
||||
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2998,14 +3067,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
"version": "2.10.42",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
|
||||
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3027,7 +3103,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
|
||||
"integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3045,11 +3123,11 @@
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"caniuse-lite": "^1.0.30001800",
|
||||
"electron-to-chromium": "^1.5.387",
|
||||
"node-releases": "^2.0.50",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -3148,7 +3226,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001766",
|
||||
"version": "1.0.30001803",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
|
||||
"integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3624,7 +3704,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.283",
|
||||
"version": "1.5.388",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz",
|
||||
"integrity": "sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -3862,6 +3944,8 @@
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4282,14 +4366,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -4615,7 +4701,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -4653,6 +4741,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/human-interval": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
@@ -5190,7 +5291,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.13.3",
|
||||
"version": "17.13.4",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz",
|
||||
"integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.3.0",
|
||||
@@ -5205,7 +5308,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -5456,6 +5571,8 @@
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@@ -5787,8 +5904,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"license": "MIT"
|
||||
"version": "2.0.50",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
|
||||
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/numbered": {
|
||||
"version": "1.1.0",
|
||||
@@ -6363,10 +6485,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.3",
|
||||
"version": "6.30.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
|
||||
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.2"
|
||||
"@remix-run/router": "1.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -6376,11 +6500,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.3",
|
||||
"version": "6.30.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
|
||||
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.2",
|
||||
"react-router": "6.30.3"
|
||||
"@remix-run/router": "1.23.3",
|
||||
"react-router": "6.30.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -6609,13 +6735,13 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@@ -6625,31 +6751,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.62.2",
|
||||
"@rollup/rollup-android-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-x64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.62.2",
|
||||
"@rollup/rollup-openbsd-x64": "4.62.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.62.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.62.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -7518,6 +7644,8 @@
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7587,9 +7715,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -7892,6 +8020,8 @@
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "3.8.1",
|
||||
"version": "3.9.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,9 +14,10 @@
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write"
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
@@ -32,7 +33,7 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"joi": "17.13.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^5.19.0",
|
||||
"mui-color-input": "^7.0.0",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 34,
|
||||
height: props.multiline ? "auto" : 34,
|
||||
fontSize: typographyLevels.base,
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
|
||||
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
|
||||
import { DefaultHttpMethod, DefaultPageSpeedStrategy } from "@/Types/Monitor";
|
||||
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||
|
||||
interface UseMonitorFormOptions {
|
||||
@@ -40,11 +40,13 @@ 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":
|
||||
@@ -133,11 +135,13 @@ export const getMonitorDefaults = (
|
||||
...getGeoCheckDefaults(data),
|
||||
type: "http",
|
||||
url: "",
|
||||
method: DefaultHttpMethod,
|
||||
ignoreTlsErrors: false,
|
||||
useAdvancedMatching: false,
|
||||
matchMethod: "",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
customUpCodes: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useStatusPageForm = ({
|
||||
const defaults: StatusPageFormData = {
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || generateDefaultUrl(),
|
||||
customDomain: data?.customDomain ?? null,
|
||||
timezone: resolveTimezone(data?.timezone),
|
||||
type: data?.type || ["uptime"],
|
||||
color: data?.color || "#13715B",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
@@ -41,8 +42,11 @@ 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";
|
||||
@@ -258,7 +262,7 @@ const CreateMonitorPage = () => {
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
const { control, watch, handleSubmit, clearErrors, trigger, reset } = form;
|
||||
const { control, watch, handleSubmit, clearErrors, trigger, reset, setValue } = form;
|
||||
|
||||
useEffect(() => {
|
||||
reset(defaults);
|
||||
@@ -270,6 +274,7 @@ const CreateMonitorPage = () => {
|
||||
const showStep = (step: number) => isEditMode || 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;
|
||||
|
||||
@@ -1091,27 +1096,144 @@ const CreateMonitorPage = () => {
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Controller
|
||||
name="useAdvancedMatching"
|
||||
name="method"
|
||||
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>
|
||||
<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 }}
|
||||
>
|
||||
{t(
|
||||
"pages.createMonitor.form.advanced.option.advancedMatching.label"
|
||||
`pages.createMonitor.form.advanced.option.method.description.${field.value ?? DefaultHttpMethod}`
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
{watchedUseAdvancedMatching && (
|
||||
<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" && (
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Controller
|
||||
name="matchMethod"
|
||||
|
||||
@@ -63,10 +63,20 @@ const getChartConfigs = (
|
||||
return configs;
|
||||
};
|
||||
|
||||
const hasAnyNetworkTraffic = (checks: HardwareCheckStats[]): boolean =>
|
||||
checks.some((c) =>
|
||||
c.net?.some((iface) => iface.bytesSentPerSecond > 0 || iface.deltaBytesRecv > 0)
|
||||
);
|
||||
// Loopback / bridge names Capture sees when it can only observe the container's
|
||||
// own network namespace. When ALL reported interfaces match this set we assume
|
||||
// the "no real interfaces visible" case and surface the notice.
|
||||
const CONTAINER_ONLY_IFACE_NAMES = new Set(["lo", "lo0", "docker0"]);
|
||||
|
||||
const onlyContainerVisibleInterfaces = (checks: HardwareCheckStats[]): boolean => {
|
||||
const named = new Set<string>();
|
||||
checks.forEach((c) => c.net?.forEach((iface) => named.add(iface.name)));
|
||||
if (named.size === 0) return false;
|
||||
for (const name of named) {
|
||||
if (!CONTAINER_ONLY_IFACE_NAMES.has(name)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const InfraNetworkCharts = ({
|
||||
checks,
|
||||
@@ -82,13 +92,14 @@ export const InfraNetworkCharts = ({
|
||||
() => getChartConfigs(theme, checks, t),
|
||||
[theme, checks, t]
|
||||
);
|
||||
const showNoTrafficNotice = chartConfigs.length > 0 && !hasAnyNetworkTraffic(checks);
|
||||
const showLoopbackOnlyNotice =
|
||||
chartConfigs.length > 0 && onlyContainerVisibleInterfaces(checks);
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{showNoTrafficNotice && (
|
||||
{showLoopbackOnlyNotice && (
|
||||
<NoticeBanner severity="warning">
|
||||
{t("pages.infrastructure.charts.labels.netNoTraffic")}
|
||||
{t("pages.infrastructure.charts.labels.loopbackOnlyNotice")}
|
||||
</NoticeBanner>
|
||||
)}
|
||||
<Grid
|
||||
|
||||
@@ -33,6 +33,26 @@ 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}
|
||||
|
||||
@@ -42,12 +42,15 @@ 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 (row.lockedAt) {
|
||||
if (isRunning(row)) {
|
||||
return t("common.labels.running");
|
||||
}
|
||||
if (row.monitorActive === true) {
|
||||
@@ -72,18 +75,6 @@ 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"),
|
||||
@@ -92,50 +83,36 @@ export const TableJobs = ({
|
||||
{
|
||||
id: "interval",
|
||||
content: t("common.table.headers.interval"),
|
||||
render: (row) => {
|
||||
let interval;
|
||||
if (row.monitorId.toString().includes("geo")) {
|
||||
interval = row.monitorGeoInterval ?? 0;
|
||||
} else {
|
||||
interval = row.repeat ?? 0;
|
||||
}
|
||||
return <Typography sx={cellSx}>{prettyMilliseconds(interval)}</Typography>;
|
||||
},
|
||||
render: (row) => (
|
||||
<Typography sx={cellSx}>{prettyMilliseconds(row.monitorInterval ?? 0)}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "lastRun",
|
||||
content: t("pages.logs.table.headers.lastRunAt"),
|
||||
id: "lastFinishedAt",
|
||||
content: t("pages.logs.table.headers.lastFinishedAt"),
|
||||
render: (row) => {
|
||||
const v = formatTimestamp(row.lastRunAt) ?? "-";
|
||||
const lastFinishedAt = formatTimestamp(row.lastFinishedAt) ?? "-";
|
||||
return (
|
||||
<Typography
|
||||
title={v}
|
||||
title={lastFinishedAt}
|
||||
sx={cellSx}
|
||||
>
|
||||
{v}
|
||||
{lastFinishedAt}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "lastRunTook",
|
||||
content: t("pages.logs.table.headers.lastRunTook"),
|
||||
id: "nextScheduledAt",
|
||||
content: t("pages.logs.table.headers.nextScheduledAt"),
|
||||
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) ?? "-";
|
||||
const nextScheduledAt = formatTimestamp(row.nextScheduledAt) ?? "-";
|
||||
return (
|
||||
<Typography
|
||||
title={v}
|
||||
title={nextScheduledAt}
|
||||
sx={cellSx}
|
||||
>
|
||||
{v}
|
||||
{nextScheduledAt}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
@@ -169,7 +146,7 @@ export const TableJobs = ({
|
||||
headers={headers}
|
||||
data={jobsWithId}
|
||||
getRowSx={(row) => ({
|
||||
...(row.lockedAt && {
|
||||
...(isRunning(row) && {
|
||||
"& td": { backgroundColor: theme.palette.rowStatus.running },
|
||||
}),
|
||||
...(row.monitorActive === false && {
|
||||
|
||||
@@ -151,12 +151,18 @@ 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);
|
||||
|
||||
@@ -284,6 +290,27 @@ const CreateStatusPage = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
@@ -504,6 +531,33 @@ const CreateStatusPage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showStep(1) && (
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.customCSS.title")}
|
||||
subtitle={t("pages.statusPages.form.customCSS.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="customCSS"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
multiline
|
||||
rows={8}
|
||||
fieldLabel={t(
|
||||
"pages.statusPages.form.customCSS.option.customCSS.label"
|
||||
)}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.customCSS.option.customCSS.placeholder"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showStep(1) && (
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.features.title")}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Settings, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import { type StatusPage } from "@/Types/StatusPage";
|
||||
import { getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
|
||||
|
||||
interface HeaderStatusPageControlsProps {
|
||||
isAdmin: boolean;
|
||||
@@ -50,7 +51,7 @@ export const HeaderStatusPageControls = ({
|
||||
<Typography
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`,
|
||||
getStatusPagePublicUrl(statusPage),
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -14,6 +15,13 @@ import {
|
||||
type StatusPageResponse,
|
||||
type StatusPageTheme,
|
||||
} from "@/Types/StatusPage";
|
||||
import {
|
||||
buildStatusPageApiPath,
|
||||
getStatusPagePreviewUrl,
|
||||
getStatusPagePublicUrl,
|
||||
isCustomDomainHost,
|
||||
} from "@/Utils/statusPageUrl";
|
||||
import { cssReferencesExternalResource } from "@/Utils/customCss";
|
||||
import { HeaderStatusPageControls } from "@/Pages/StatusPage/Status/Components/HeaderStatusPageControls";
|
||||
import { StatusPageThemeProvider } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
import {
|
||||
@@ -33,6 +41,7 @@ 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: {
|
||||
@@ -58,6 +67,11 @@ const THEME_CONFIGS: Record<StatusPageTheme, ThemeConfig<any>> = {
|
||||
HeroSlot: EditorialHero,
|
||||
overallStatusOptions: { allUpKey: "pages.statusPages.editorial.allUp" },
|
||||
},
|
||||
minimal: {
|
||||
createStyles: minimalStyles,
|
||||
HeaderSlot: RefinedHeader,
|
||||
HeroSlot: RefinedHero,
|
||||
},
|
||||
};
|
||||
|
||||
const StatusPageView = () => {
|
||||
@@ -67,9 +81,14 @@ const StatusPageView = () => {
|
||||
const isAdmin = useIsAdmin();
|
||||
const location = useLocation();
|
||||
|
||||
const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
|
||||
const onCustomDomainHost = isCustomDomainHost();
|
||||
const isPublic =
|
||||
onCustomDomainHost || location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
|
||||
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||
const apiUrl = buildStatusPageApiPath({
|
||||
url,
|
||||
useCustomDomain: onCustomDomainHost,
|
||||
});
|
||||
|
||||
const { data, isLoading, error } = useGet<StatusPageResponse>(
|
||||
apiUrl,
|
||||
@@ -111,30 +130,33 @@ 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)];
|
||||
const customCss =
|
||||
statusPage.customCSS && !cssReferencesExternalResource(statusPage.customCSS)
|
||||
? statusPage.customCSS
|
||||
: "";
|
||||
return (
|
||||
<StatusPageThemeProvider
|
||||
theme={statusPage.theme}
|
||||
themeMode={statusPage.themeMode}
|
||||
timezone={statusPage.timezone}
|
||||
brandColor={statusPage.color}
|
||||
paintBody
|
||||
>
|
||||
{themedRenderer}
|
||||
{customCss && <style>{customCss}</style>}
|
||||
<BaseStatusPage
|
||||
statusPage={statusPage}
|
||||
monitors={monitors}
|
||||
config={themeConfig}
|
||||
/>
|
||||
</StatusPageThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
const publicUrl = getStatusPagePublicUrl(statusPage);
|
||||
const previewUrl = getStatusPagePreviewUrl(statusPage);
|
||||
return (
|
||||
<BasePage
|
||||
loading={isLoading}
|
||||
@@ -153,7 +175,16 @@ const StatusPageView = () => {
|
||||
timezone={statusPage.timezone}
|
||||
transparent
|
||||
>
|
||||
<BrowserFrame url={publicUrl}>{themedRenderer}</BrowserFrame>
|
||||
<BrowserFrame url={publicUrl}>
|
||||
<Box
|
||||
component="iframe"
|
||||
src={previewUrl}
|
||||
title={t("pages.statusPages.preview.title")}
|
||||
flex={1}
|
||||
width="100%"
|
||||
border={0}
|
||||
/>
|
||||
</BrowserFrame>
|
||||
</StatusPageThemeProvider>
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,13 @@ export const BrowserFrame = ({ url, children }: Props) => {
|
||||
{url}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minHeight: 0 }}>{children}</Box>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
sx={{ flex: 1, minHeight: 0 }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { alpha, darken } from "@mui/material/styles";
|
||||
import {
|
||||
DEFAULT_STATUS_PAGE_THEME,
|
||||
resolveStatusPageTheme,
|
||||
@@ -36,6 +37,15 @@ interface Props {
|
||||
theme?: StatusPageTheme;
|
||||
themeMode?: StatusPageThemeMode;
|
||||
timezone?: string;
|
||||
/**
|
||||
* Brand color picked in the status-page appearance settings. When
|
||||
* provided, it feeds `tokens.brand` / `brandStrong` / `brandSoft`,
|
||||
* which theme styles use for accent surfaces (hero, active tabs,
|
||||
* links). It does NOT recolor the semantic `up`/OK status color, so
|
||||
* a red or orange brand cannot make the "all operational" indicator
|
||||
* signal downtime.
|
||||
*/
|
||||
brandColor?: string;
|
||||
/**
|
||||
* When true, paint the document body with the theme background so it
|
||||
* covers beyond the provider's wrapper (useful on the public route
|
||||
@@ -54,10 +64,32 @@ interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
// Resolve the brand triple that theme styles read from. A customer's
|
||||
// picked color (when valid) overrides the theme's default; otherwise
|
||||
// the theme's own `brand*` values win, and finally we fall back to the
|
||||
// `up*` triple so unmigrated theme styles have something to consume.
|
||||
const resolveBrandTokens = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
brandColor: string | undefined
|
||||
): StatusPageThemeTokens => {
|
||||
const validOverride = brandColor && HEX_COLOR.test(brandColor) ? brandColor : undefined;
|
||||
const brand = validOverride ?? tokens.brand ?? tokens.up;
|
||||
const brandStrong = validOverride
|
||||
? darken(validOverride, 0.2)
|
||||
: (tokens.brandStrong ?? tokens.upStrong);
|
||||
const brandSoft = validOverride
|
||||
? alpha(validOverride, 0.15)
|
||||
: (tokens.brandSoft ?? tokens.upSoft);
|
||||
return { ...tokens, brand, brandStrong, brandSoft };
|
||||
};
|
||||
|
||||
export const StatusPageThemeProvider = ({
|
||||
theme,
|
||||
themeMode,
|
||||
timezone,
|
||||
brandColor,
|
||||
paintBody = false,
|
||||
transparent = false,
|
||||
children,
|
||||
@@ -109,7 +141,11 @@ export const StatusPageThemeProvider = ({
|
||||
|
||||
const resolvedMode: ResolvedMode =
|
||||
resolvedThemeMode === "auto" ? systemMode : resolvedThemeMode;
|
||||
const tokens = themeTokens[resolvedTheme][resolvedMode];
|
||||
const baseTokens = themeTokens[resolvedTheme][resolvedMode];
|
||||
const tokens = useMemo(
|
||||
() => resolveBrandTokens(baseTokens, brandColor),
|
||||
[baseTokens, brandColor]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -99,9 +99,9 @@ export const boldStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "36px",
|
||||
},
|
||||
brand: {
|
||||
@@ -130,7 +130,7 @@ export const boldStyles = (
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "22px",
|
||||
padding: "36px 36px 32px",
|
||||
padding: { xs: "24px 20px", md: "36px 36px 32px" },
|
||||
mb: "28px",
|
||||
background: isDark ? heroBgDark : heroBgLight,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
@@ -149,13 +149,14 @@ export const boldStyles = (
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
fontSize: 34,
|
||||
fontSize: { xs: 24, md: 34 },
|
||||
fontWeight: 800,
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1.1,
|
||||
color: tokens.text,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
},
|
||||
heroCheck: (tone) => ({
|
||||
@@ -171,9 +172,15 @@ export const boldStyles = (
|
||||
}),
|
||||
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "16px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "16px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
@@ -181,16 +188,20 @@ export const boldStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "8px 18px",
|
||||
padding: { xs: "8px 6px", md: "8px 18px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
letterSpacing: { xs: "0.04em", md: "0.12em" },
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
@@ -217,13 +228,21 @@ export const boldStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "24px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "24px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
p: "22px 28px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 700,
|
||||
fontSize: 17,
|
||||
letterSpacing: "-0.01em",
|
||||
@@ -233,10 +252,9 @@ export const boldStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -273,6 +291,8 @@ export const boldStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
padding: "6px 14px",
|
||||
@@ -296,10 +316,10 @@ export const boldStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 28px 22px",
|
||||
padding: { xs: "0 12px 18px", md: "0 28px 22px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 48,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -312,10 +332,10 @@ export const boldStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 28px",
|
||||
padding: { xs: "0 12px", md: "0 28px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 48,
|
||||
},
|
||||
|
||||
@@ -90,9 +90,9 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
alignItems: { xs: "flex-start", md: "flex-end" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "48px",
|
||||
borderBottom: `2px solid ${tokens.text}`,
|
||||
pb: "18px",
|
||||
@@ -119,7 +119,7 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
statusLine: {
|
||||
fontSize: 22,
|
||||
fontSize: { xs: 18, md: 22 },
|
||||
fontWeight: 400,
|
||||
m: 0,
|
||||
mb: "8px",
|
||||
@@ -144,26 +144,32 @@ export const editorialStyles = (
|
||||
letterSpacing: "0.12em",
|
||||
color: tokens.textMuted,
|
||||
m: 0,
|
||||
mb: "56px",
|
||||
mb: { xs: "32px", md: "56px" },
|
||||
},
|
||||
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "20px",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.text}`,
|
||||
"& > button + button": { borderLeft: `1px solid ${tokens.text}` },
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.2em",
|
||||
padding: "6px 14px",
|
||||
letterSpacing: { xs: "0.05em", md: "0.2em" },
|
||||
padding: { xs: "6px 6px", md: "6px 14px" },
|
||||
border: 0,
|
||||
background: active ? tokens.text : "transparent",
|
||||
color: active ? tokens.bg : tokens.text,
|
||||
@@ -188,12 +194,20 @@ export const editorialStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "baseline",
|
||||
gap: "20px",
|
||||
alignItems: { xs: "start", md: "baseline" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "20px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.015em",
|
||||
@@ -203,14 +217,13 @@ export const editorialStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
gridArea: "meta",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
mt: "6px",
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
},
|
||||
pill: {
|
||||
textTransform: "uppercase",
|
||||
@@ -234,6 +247,8 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
@@ -250,7 +265,7 @@ export const editorialStyles = (
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
gap: { xs: "1px", md: "2px" },
|
||||
height: 40,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -262,7 +277,7 @@ export const editorialStyles = (
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
gap: { xs: "1px", md: "2px" },
|
||||
alignItems: "flex-end",
|
||||
height: 40,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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.pulse(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box sx={styles.heroRow}>
|
||||
<Box sx={styles.pulse(overall.tone)} />
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export interface ModernStyles {
|
||||
logoImg: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
pulse: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroRow: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
@@ -117,9 +117,9 @@ export const modernStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "32px",
|
||||
},
|
||||
brand: {
|
||||
@@ -148,11 +148,11 @@ export const modernStyles = (
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "20px",
|
||||
padding: "28px 32px",
|
||||
padding: { xs: "20px", md: "28px 32px" },
|
||||
mb: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
boxShadow: cardShadow,
|
||||
@@ -182,25 +182,37 @@ export const modernStyles = (
|
||||
},
|
||||
};
|
||||
},
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: { xs: "14px", md: "20px" },
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "4px",
|
||||
fontSize: 22,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: { xs: 18, md: 22 },
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
color: tokens.text,
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
display: { xs: "none", md: "flex" },
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "14px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "14px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
@@ -208,16 +220,20 @@ export const modernStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "6px 16px",
|
||||
padding: { xs: "6px 6px", md: "6px 16px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
letterSpacing: { xs: "0.04em", md: "0.08em" },
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
@@ -256,13 +272,21 @@ export const modernStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "20px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
p: "18px 24px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
letterSpacing: "-0.005em",
|
||||
@@ -272,10 +296,9 @@ export const modernStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -299,6 +322,8 @@ export const modernStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
padding: "5px 12px",
|
||||
@@ -319,10 +344,10 @@ export const modernStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 24px 20px",
|
||||
padding: { xs: "0 12px 16px", md: "0 24px 20px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 46,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -334,10 +359,10 @@ export const modernStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 24px",
|
||||
padding: { xs: "0 12px", md: "0 24px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 46,
|
||||
},
|
||||
|
||||
@@ -11,24 +11,24 @@ export const RefinedHero = ({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box sx={styles.heroRow}>
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export interface RefinedStyles {
|
||||
company: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
statusDot: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroRow: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
@@ -102,9 +102,9 @@ export const refinedStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "28px",
|
||||
},
|
||||
brand: {
|
||||
@@ -119,7 +119,7 @@ export const refinedStyles = (
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: "8px",
|
||||
background: tokens.up,
|
||||
background: tokens.brand,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#fff",
|
||||
@@ -133,10 +133,10 @@ export const refinedStyles = (
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: tokens.radius,
|
||||
padding: "22px 24px",
|
||||
padding: { xs: "18px 20px", md: "22px 24px" },
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
boxShadow: cardShadow,
|
||||
mb: "20px",
|
||||
},
|
||||
@@ -148,10 +148,15 @@ export const refinedStyles = (
|
||||
boxShadow: `0 0 0 4px ${toneSoft(tone, tokens)}`,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: { xs: "12px", md: "16px" },
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "2px",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
@@ -159,14 +164,21 @@ export const refinedStyles = (
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
display: { xs: "none", md: "flex" },
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "12px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "12px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "8px",
|
||||
background: tokens.surface,
|
||||
@@ -174,17 +186,21 @@ export const refinedStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
background: active ? tokens.brandSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "5px 14px",
|
||||
padding: { xs: "5px 6px", md: "5px 14px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
color: active ? tokens.brand : tokens.textMuted,
|
||||
borderRadius: "5px",
|
||||
transition: "background 0.15s ease, color 0.15s ease",
|
||||
fontWeight: active ? 600 : 500,
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
"&:hover": { color: active ? tokens.brand : tokens.text },
|
||||
}),
|
||||
|
||||
monitorList: {
|
||||
@@ -207,13 +223,21 @@ export const refinedStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "16px" },
|
||||
rowGap: { xs: "10px", md: "4px" },
|
||||
p: "16px 20px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
letterSpacing: "-0.005em",
|
||||
@@ -223,10 +247,9 @@ export const refinedStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "4px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -236,9 +259,9 @@ export const refinedStyles = (
|
||||
},
|
||||
pillHardware: {
|
||||
...pillBase,
|
||||
color: tokens.up,
|
||||
color: tokens.brand,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
background: tokens.upSoft,
|
||||
background: tokens.brandSoft,
|
||||
},
|
||||
monitorUrl: {
|
||||
fontSize: 12,
|
||||
@@ -251,6 +274,8 @@ export const refinedStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: "4px 10px",
|
||||
@@ -261,10 +286,10 @@ export const refinedStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 20px 16px",
|
||||
padding: { xs: "0 10px 14px", md: "0 20px 16px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 42,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -276,10 +301,10 @@ export const refinedStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 20px",
|
||||
padding: { xs: "0 10px", md: "0 20px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 42,
|
||||
},
|
||||
@@ -357,11 +382,11 @@ export const refinedStyles = (
|
||||
fontSize: 12,
|
||||
mt: "40px",
|
||||
"& a": {
|
||||
color: tokens.up,
|
||||
color: tokens.brand,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "3px",
|
||||
fontWeight: 600,
|
||||
"&:hover": { color: tokens.upStrong || tokens.up },
|
||||
"&:hover": { color: tokens.brandStrong },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
statusBadgeKey,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import { useStatusPageTheme } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
import { formatPercentage } from "@/Utils/FormatUtils";
|
||||
|
||||
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
|
||||
|
||||
@@ -101,8 +102,10 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
|
||||
return (
|
||||
<Box sx={styles.page}>
|
||||
<Box
|
||||
{statusPage.customCSS && <style>{statusPage.customCSS}</style>}
|
||||
<Stack
|
||||
component="header"
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
sx={styles.top}
|
||||
>
|
||||
<HeaderSlot
|
||||
@@ -112,7 +115,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
monitorCount={monitors.length}
|
||||
styles={styles}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<HeroSlot
|
||||
statusPage={statusPage}
|
||||
@@ -161,6 +164,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
const showInfra = isHardware && statusPage.showInfrastructure !== false;
|
||||
const showChart = !isHardware && statusPage.showCharts !== false;
|
||||
const badgeTone = monitorBadgeTone(monitor.status);
|
||||
const uptimePercentage = formatPercentage(monitor.uptimePercentage ?? 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -169,32 +173,39 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
sx={styles.card}
|
||||
>
|
||||
<Box sx={styles.cardRow}>
|
||||
<Box sx={styles.cardLeft}>
|
||||
<Box sx={styles.monitorName}>{monitor.name}</Box>
|
||||
<Box sx={styles.monitorMeta}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={isHardware ? styles.pillHardware : styles.pill}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Box>
|
||||
{monitor.url && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.monitorUrl}
|
||||
title={monitor.url}
|
||||
>
|
||||
{monitor.url}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={styles.monitorName}>{monitor.name}</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.badge(badgeTone)}
|
||||
>
|
||||
{t(statusBadgeKey[monitor.status])}
|
||||
</Box>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
sx={styles.monitorMeta}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={isHardware ? styles.pillHardware : styles.pill}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ ...styles.pill }}
|
||||
>
|
||||
{uptimePercentage}
|
||||
</Box>
|
||||
{monitor.url && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.monitorUrl}
|
||||
title={monitor.url}
|
||||
>
|
||||
{monitor.url}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{showInfra && (
|
||||
|
||||
@@ -9,6 +9,14 @@ export interface StatusPageThemeTokens {
|
||||
up: string;
|
||||
upStrong: string;
|
||||
upSoft: string;
|
||||
// Accent used for brand-tinted surfaces (hero background, active tab
|
||||
// underline, "view all" links). Distinct from `up` so a red/orange
|
||||
// brand color cannot recolor the semantic OK/up status indicators.
|
||||
// Themes leave these undefined by default; the provider falls back
|
||||
// to `up`/`upStrong`/`upSoft` so styles have a value to consume.
|
||||
brand?: string;
|
||||
brandStrong?: string;
|
||||
brandSoft?: string;
|
||||
degraded: string;
|
||||
degradedSoft: string;
|
||||
down: string;
|
||||
@@ -191,9 +199,47 @@ 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,7 +11,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import { getStatusPagePublicPath, getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
|
||||
|
||||
import { type StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface StatusPagesTableProps {
|
||||
data: StatusPage[];
|
||||
@@ -53,8 +55,7 @@ export const StatusPagesTable = ({
|
||||
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
|
||||
if (row.isPublished) {
|
||||
e.stopPropagation();
|
||||
const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
window.open(getStatusPagePublicUrl(row), "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +71,9 @@ export const StatusPagesTable = ({
|
||||
content: t("pages.statusPages.table.headers.url"),
|
||||
render: (row) => {
|
||||
const content = row.isPublished
|
||||
? `/${row.url}`
|
||||
? row.customDomain
|
||||
? getStatusPagePublicPath(row)
|
||||
: `/${row.url}`
|
||||
: t("pages.statusPages.table.unpublished");
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -25,6 +25,8 @@ 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[];
|
||||
@@ -259,6 +261,24 @@ 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"),
|
||||
@@ -304,7 +324,9 @@ export const MonitorTable = ({
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.type;
|
||||
return row.type === "http" && row.method
|
||||
? `${row.type} (${row.method})`
|
||||
: row.type;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,8 +54,24 @@ import Logs from "@/Pages/Logs";
|
||||
import { ProtectedRoute, RoleProtectedRoute } from "@/Components/routing/RouteProtected";
|
||||
|
||||
import CreateMonitor from "@/Pages/CreateMonitor";
|
||||
import { isCustomDomainHost } from "@/Utils/statusPageUrl";
|
||||
|
||||
const Routes = () => {
|
||||
if (isCustomDomainHost()) {
|
||||
return (
|
||||
<LibRoutes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Status />}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<Status />}
|
||||
/>
|
||||
</LibRoutes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LibRoutes>
|
||||
<Route
|
||||
|
||||
@@ -23,6 +23,11 @@ export interface V8HeapStats {
|
||||
|
||||
export interface MongoStats {
|
||||
readyState: number;
|
||||
readsPerSecond: number;
|
||||
insertsPerSecond: number;
|
||||
updatesPerSecond: number;
|
||||
deletesPerSecond: number;
|
||||
writesPerSecond: number;
|
||||
host: string;
|
||||
port: number;
|
||||
dbName: string;
|
||||
|
||||
@@ -48,6 +48,10 @@ 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;
|
||||
@@ -64,6 +68,7 @@ export interface Monitor {
|
||||
jsonPath?: string;
|
||||
expectedValue?: string;
|
||||
matchMethod?: MonitorMatchMethod;
|
||||
method?: HttpMethod;
|
||||
url: string;
|
||||
port?: number;
|
||||
isActive: boolean;
|
||||
@@ -71,6 +76,7 @@ export interface Monitor {
|
||||
uptimePercentage?: number;
|
||||
notifications: string[];
|
||||
tags: string[];
|
||||
customUpCodes?: number[];
|
||||
secret?: string;
|
||||
cpuAlertThreshold: number;
|
||||
cpuAlertCounter: number;
|
||||
|
||||
@@ -30,23 +30,16 @@ export type QueueMetrics = {
|
||||
|
||||
export type QueueJobSummary = {
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: string | null;
|
||||
monitorInterval: number | null;
|
||||
monitorGeoInterval: number | null;
|
||||
monitorActive: boolean | null;
|
||||
active: boolean;
|
||||
lockedBy: string | null;
|
||||
lockedUntil: number | null;
|
||||
lockedAt: number | null;
|
||||
nextScheduledAt: number;
|
||||
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 {
|
||||
|
||||
@@ -21,7 +21,13 @@ export const getMonitorTypeLabel = (
|
||||
return key ? t(key) : type;
|
||||
};
|
||||
|
||||
export const STATUS_PAGE_THEMES = ["refined", "modern", "bold", "editorial"] as const;
|
||||
export const STATUS_PAGE_THEMES = [
|
||||
"refined",
|
||||
"modern",
|
||||
"bold",
|
||||
"editorial",
|
||||
"minimal",
|
||||
] as const;
|
||||
export type StatusPageTheme = (typeof STATUS_PAGE_THEMES)[number];
|
||||
export const DEFAULT_STATUS_PAGE_THEME: StatusPageTheme = "refined";
|
||||
|
||||
@@ -47,6 +53,7 @@ export interface StatusPage {
|
||||
type: MonitorDisplayType[];
|
||||
companyName: string;
|
||||
url: string;
|
||||
customDomain?: string | null;
|
||||
timezone?: string;
|
||||
color: string;
|
||||
monitors: string[];
|
||||
|
||||
@@ -63,6 +63,16 @@ export const getResponseTimeColor = (responseTime: number): PaletteKey => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUptimePercentageColor = (uptimePercentage: number): PaletteKey => {
|
||||
if (uptimePercentage >= 0.75) {
|
||||
return "success";
|
||||
} else if (uptimePercentage >= 0.5) {
|
||||
return "warning";
|
||||
} else {
|
||||
return "error";
|
||||
}
|
||||
};
|
||||
|
||||
export const getInfraGaugeColor = (val: number, theme: any) => {
|
||||
if (val < 50) {
|
||||
return theme.palette.success.main;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
);
|
||||
};
|
||||
@@ -74,3 +74,96 @@ 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)" },
|
||||
];
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,7 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import type { FieldPath } from "react-hook-form";
|
||||
import { GeoContinents } from "@/Types/GeoCheck";
|
||||
import { DnsRecordTypes, PageSpeedStrategies, type MonitorType } from "@/Types/Monitor";
|
||||
import {
|
||||
DnsRecordTypes,
|
||||
HttpMethods,
|
||||
PageSpeedStrategies,
|
||||
type MonitorType,
|
||||
} from "@/Types/Monitor";
|
||||
import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode";
|
||||
|
||||
// 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
|
||||
@@ -53,9 +59,15 @@ 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
|
||||
@@ -64,6 +76,10 @@ 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,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -9,6 +10,23 @@ import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage"
|
||||
// field definitions.
|
||||
export const statusPageStepRegistry = z.registry<{ step: number }>();
|
||||
|
||||
const statusPageHostnameRegex =
|
||||
/^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
|
||||
|
||||
const normalizeCustomDomainInput = (raw: string | null): string | null => {
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutScheme = trimmed.replace(/^https?:\/\//, "");
|
||||
return withoutScheme.split("/")[0]?.split(":")[0] ?? null;
|
||||
};
|
||||
|
||||
export const statusPageSchema = z.object({
|
||||
companyName: z
|
||||
.string()
|
||||
@@ -22,6 +40,12 @@ 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"]))
|
||||
@@ -35,7 +59,14 @@ export const statusPageSchema = z.object({
|
||||
showUptimePercentage: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
||||
showAdminLoginLink: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
||||
showInfrastructure: z.boolean().register(statusPageStepRegistry, { step: 1 }),
|
||||
customCSS: z.string().optional().register(statusPageStepRegistry, { step: 1 }),
|
||||
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()
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "معرّف المراقب",
|
||||
"runCount": "عدد التشغيلات",
|
||||
"failCount": "عدد حالات الفشل",
|
||||
"lastRunAt": "آخر تشغيل في",
|
||||
"lockedAt": "مقفل في",
|
||||
"lastFinishedAt": "آخر انتهاء في",
|
||||
"lastRunTook": "مدة آخر تشغيل",
|
||||
"lastFailedAt": "آخر فشل في",
|
||||
"failReason": "سبب الفشل",
|
||||
"state": "الحالة"
|
||||
|
||||
@@ -945,10 +945,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -490,6 +490,13 @@
|
||||
"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"
|
||||
},
|
||||
@@ -505,6 +512,11 @@
|
||||
"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"
|
||||
@@ -832,7 +844,7 @@
|
||||
"memory": "Memory usage",
|
||||
"netBytesRecv": "{{name}} - Bytes Received",
|
||||
"netBytesSent": "{{name}} - Bytes Sent",
|
||||
"netNoTraffic": "Capture is reporting zero traffic on every interface. This is normal when Capture runs inside a container that can only see loopback interfaces — run Capture directly on the host to see real network activity.",
|
||||
"loopbackOnlyNotice": "Capture is only reporting loopback or container-bridge interfaces (lo, lo0, docker0). This is normal when Capture runs inside a container that can only see its own network namespace — run Capture directly on the host to see real network activity.",
|
||||
"temp": "Temp"
|
||||
}
|
||||
},
|
||||
@@ -975,6 +987,11 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1015,11 +1032,9 @@
|
||||
"monitorId": "Monitor ID",
|
||||
"runCount": "Run count",
|
||||
"failCount": "Count",
|
||||
"lastRunAt": "Last run at",
|
||||
"lockedAt": "Locked at",
|
||||
"nextScheduledAt": "Next run at",
|
||||
"lockedBy": "Locked by",
|
||||
"lastFinishedAt": "Last finished at",
|
||||
"lastRunTook": "Last run took",
|
||||
"lastFailedAt": "Last failed",
|
||||
"failReason": "Fail reason"
|
||||
}
|
||||
@@ -1509,6 +1524,11 @@
|
||||
"url": {
|
||||
"label": "Your status page address",
|
||||
"placeholder": "my-status-page"
|
||||
},
|
||||
"customDomain": {
|
||||
"label": "Custom domain",
|
||||
"placeholder": "status.example.com",
|
||||
"helper": "Optional. Point a DNS record to this Checkmate instance to serve the status page at your own domain."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1565,6 +1585,10 @@
|
||||
"editorial": {
|
||||
"name": "Editorial",
|
||||
"description": "Serif headings and hairline dividers. Distinctive and calm."
|
||||
},
|
||||
"minimal": {
|
||||
"name": "Minimal",
|
||||
"description": "Flat, focused, and compact with only the essentials."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1574,6 +1598,16 @@
|
||||
"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.",
|
||||
@@ -1596,6 +1630,9 @@
|
||||
"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": {
|
||||
@@ -1607,7 +1644,8 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"responseTime": "Response time"
|
||||
"responseTime": "Response time",
|
||||
"uptime": "Uptime %"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "モニターID",
|
||||
"runCount": "実行回数",
|
||||
"failCount": "失敗回数",
|
||||
"lastRunAt": "最終実行日時",
|
||||
"lockedAt": "ロック日時",
|
||||
"lastFinishedAt": "最終完了日時",
|
||||
"lastRunTook": "最終実行時間",
|
||||
"lastFailedAt": "最終失敗日時",
|
||||
"failReason": "失敗理由",
|
||||
"state": "状態"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "ID монитора",
|
||||
"runCount": "Кол-во запусков",
|
||||
"failCount": "Кол-во ошибок",
|
||||
"lastRunAt": "Последний запуск",
|
||||
"lockedAt": "Заблокировано в",
|
||||
"lastFinishedAt": "Последнее завершение",
|
||||
"lastRunTook": "Последний запуск занял",
|
||||
"lastFailedAt": "Последняя ошибка",
|
||||
"failReason": "Причина ошибки",
|
||||
"state": "Состояние"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "รหัสมอนิเตอร์",
|
||||
"runCount": "จำนวนรัน",
|
||||
"failCount": "จำนวนล้มเหลว",
|
||||
"lastRunAt": "รันล่าสุดเมื่อ",
|
||||
"lockedAt": "ล็อกเมื่อ",
|
||||
"lastFinishedAt": "เสร็จสิ้นล่าสุดเมื่อ",
|
||||
"lastRunTook": "รันล่าสุดใช้เวลา",
|
||||
"lastFailedAt": "ล้มเหลวล่าสุดเมื่อ",
|
||||
"failReason": "สาเหตุที่ล้มเหลว",
|
||||
"state": "สถานะ"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "ID монітора",
|
||||
"runCount": "Кількість запусків",
|
||||
"failCount": "Кількість помилок",
|
||||
"lastRunAt": "Останній запуск",
|
||||
"lockedAt": "Заблоковано о",
|
||||
"lastFinishedAt": "Останнє завершення",
|
||||
"lastRunTook": "Останній запуск тривав",
|
||||
"lastFailedAt": "Остання помилка",
|
||||
"failReason": "Причина помилки",
|
||||
"state": "Стан"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"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"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "监控 ID",
|
||||
"runCount": "运行次数",
|
||||
"failCount": "失败次数",
|
||||
"lastRunAt": "上次运行于",
|
||||
"lockedAt": "锁定于",
|
||||
"lastFinishedAt": "上次完成于",
|
||||
"lastRunTook": "上次运行耗时",
|
||||
"lastFailedAt": "上次失败于",
|
||||
"failReason": "失败原因",
|
||||
"state": "状态"
|
||||
|
||||
@@ -941,10 +941,7 @@
|
||||
"monitorId": "監控 ID",
|
||||
"runCount": "執行次數",
|
||||
"failCount": "失敗次數",
|
||||
"lastRunAt": "最後執行於",
|
||||
"lockedAt": "鎖定於",
|
||||
"lastFinishedAt": "最後完成於",
|
||||
"lastRunTook": "最後執行耗時",
|
||||
"lastFailedAt": "最後失敗於",
|
||||
"failReason": "失敗原因",
|
||||
"state": "狀態"
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
!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
|
||||
|
||||
|
||||
@@ -12,14 +12,11 @@ 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_redis
|
||||
set dockerfiles[2]=.\docker\coolify\redis.Dockerfile
|
||||
|
||||
set services[3]=bluewaveuptime/uptime_server
|
||||
set dockerfiles[3]=.\docker\coolify\server.Dockerfile
|
||||
set services[2]=bluewaveuptime/uptime_server
|
||||
set dockerfiles[2]=.\docker\coolify\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2,3) do (
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ 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"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -19,7 +19,6 @@ 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:
|
||||
|
||||
@@ -20,9 +20,35 @@ 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,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -9,7 +9,6 @@ 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:
|
||||
|
||||
@@ -19,7 +19,7 @@ RUN npm run build
|
||||
# ---------------------
|
||||
# Backend stage
|
||||
# ---------------------
|
||||
FROM node:20-slim AS backend
|
||||
FROM node:22-slim AS backend
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim AS frontend-build
|
||||
FROM node:22-slim AS frontend-build
|
||||
|
||||
WORKDIR /app/client
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN npm install
|
||||
COPY client ./
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-slim AS app
|
||||
FROM node:22-slim AS app
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
|
||||
|
||||
<p align="center">
|
||||
🇺🇸 <a href="../../README.md">English</a> |
|
||||
🇸🇦 <a href="README.ar.md">العربية</a> |
|
||||
🇪🇸 <a href="README.ca.md">Català</a> |
|
||||
🇨🇿 <a href="README.cs.md">Čeština</a> |
|
||||
🇩🇪 <a href="README.de.md">Deutsch</a> |
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
🇹🇭 <a href="README.th.md">ไทย</a> |
|
||||
🇹🇷 <a href="README.tr.md">Türkçe</a> |
|
||||
🇺🇦 <a href="README.uk.md">Українська</a> |
|
||||
🇻🇳 <a href="README.vi.md">Tiếng Việt</a> |
|
||||
🇨🇳 <a href="README.zh-CN.md">简体中文</a> |
|
||||
🇹🇼 <a href="README.zh-TW.md">繁體中文</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://deepwiki.com/bluewave-labs/checkmate)
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Un'applicazione open source per il monitoraggio di uptime e infrastruttura</strong></p>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
Questo repository contiene sia il frontend che il backend di Checkmate, uno strumento di monitoraggio open source e self-hosted per tenere sotto controllo l'hardware dei server, l'uptime, i tempi di risposta e gli incidenti in tempo reale, con visualizzazioni accattivanti. Checkmate verifica regolarmente se un server o un sito web è raggiungibile e funziona in modo ottimale, fornendo avvisi e report in tempo reale su disponibilità, tempi di inattività e tempi di risposta dei servizi monitorati.
|
||||
|
||||
Checkmate dispone anche di un agente, chiamato [Capture](https://github.com/bluewave-labs/capture), per raccogliere dati dai server remoti. Anche se Capture non è necessario per far funzionare Checkmate, offre informazioni aggiuntive su CPU, RAM, disco e temperatura dei tuoi server. Capture può essere eseguito su Linux, Windows, Mac, Raspberry Pi o qualsiasi altro dispositivo in grado di eseguire Go.
|
||||
|
||||
Checkmate è stato sottoposto a stress test con oltre 1000 monitor attivi senza problemi particolari né colli di bottiglia nelle prestazioni.
|
||||
|
||||
## 📚 Indice
|
||||
|
||||
- [📦 Demo](#demo)
|
||||
- [🔗 Guida utente](#users-guide)
|
||||
- [🛠️ Installazione](#installation)
|
||||
- [🚀 Prestazioni](#performance)
|
||||
- [💚 Domande e idee](#questions--ideas)
|
||||
- [🧩 Funzionalità](#features)
|
||||
- [🏗️ Screenshot](#screenshots)
|
||||
- [🏗️ Tech stack](#tech-stack)
|
||||
- [🔗 Alcuni link](#a-few-links)
|
||||
- [🤝 Come contribuire](#contributing)
|
||||
|
||||
|
||||
<a id="demo"></a>
|
||||
## Demo
|
||||
|
||||
Puoi vedere l'ultima build di [Checkmate](https://demo.checkmate.so/) in azione.
|
||||
|
||||
Il nome utente è demouser@demo.com e la password è Demouser1! (una nota: aggiorniamo il server demo di tanto in tanto, quindi se non dovesse funzionare, scrivici sul canale Discussions).
|
||||
|
||||
<a id="users-guide"></a>
|
||||
## Guida utente
|
||||
|
||||
Le istruzioni per l'uso sono disponibili [qui](https://checkmate.so/docs).
|
||||
|
||||
## Prerequisiti
|
||||
- [Docker](https://www.docker.com/) installato
|
||||
- [Git](https://git-scm.com/) installato
|
||||
|
||||
<a id="installation"></a>
|
||||
## Installazione
|
||||
|
||||
Consulta le istruzioni di installazione nel [portale della documentazione di Checkmate](https://checkmate.so/docs).
|
||||
|
||||
In alternativa, puoi usare [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudafrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per creare rapidamente un'istanza di Checkmate. Se desideri monitorare l'infrastruttura dei tuoi server, ti servirà l'[agente Capture](https://github.com/bluewave-labs/capture). Anche il repository di Capture contiene le istruzioni di installazione.
|
||||
|
||||
### Usare una CA personalizzata
|
||||
|
||||
Se hai bisogno di monitorare endpoint HTTPS interni con certificati emessi da Autorità di Certificazione (CA) private (come Smallstep), consulta la nostra [Guida all'attendibilità delle CA personalizzate](../custom-ca-trust.md) per le opzioni di configurazione di Docker.
|
||||
|
||||
Per ulteriore documentazione, consulta la [cartella docs](../).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Prestazioni
|
||||
|
||||
Grazie a numerose ottimizzazioni, Checkmate opera con un consumo di memoria estremamente ridotto, richiedendo risorse minime di memoria e CPU. Ecco l'utilizzo di memoria di un'istanza Node.js in esecuzione su un server che monitora 323 server ogni minuto:
|
||||
|
||||

|
||||
|
||||
Puoi vedere il consumo di memoria di MongoDB e Redis sullo stesso server (398Mb e 15Mb) per lo stesso numero di server:
|
||||
|
||||

|
||||
|
||||
<a id="questions--ideas"></a>
|
||||
## Domande e idee
|
||||
|
||||
Se hai domande, suggerimenti o commenti, hai diverse opzioni:
|
||||
|
||||
- [Canale Discord](https://discord.gg/NAb6H3UTjK) (preferito)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (controlliamo di tanto in tanto)
|
||||
|
||||
Non esitare a fare domande o a condividere le tue idee: ci farebbe piacere sentirti!
|
||||
|
||||
<a id="features"></a>
|
||||
## Funzionalità
|
||||
|
||||
- Completamente open source, distribuibile sui tuoi server o su dispositivi domestici (es. Raspberry Pi 4 o 5)
|
||||
- Diverse opzioni di monitoraggio: Uptime, Docker, Ping, SSL, Port, Game server
|
||||
- Monitoraggio della velocità delle pagine
|
||||
- Monitoraggio dell'infrastruttura (memoria, utilizzo del disco, prestazioni della CPU, rete, ecc.) - richiede l'agente [Capture](https://github.com/bluewave-labs/capture)
|
||||
- Monitoraggio selettivo dei dischi con selezione del mountpoint
|
||||
- Incidenti a colpo d'occhio
|
||||
- Pagine di stato con 4 temi curati
|
||||
- Notifiche via E-mail, Webhook, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS)
|
||||
- Manutenzione programmata
|
||||
- Monitoraggio tramite query JSON
|
||||
- Supporto multilingua per arabo, cinese (semplificato), cinese (tradizionale, Taiwan), ceco, inglese, finlandese, francese, tedesco, giapponese, portoghese (Brasile), russo, spagnolo, thailandese, turco, ucraino e vietnamita
|
||||
|
||||
|
||||
## Ciclo di vita del monitor
|
||||
|
||||
1. Un monitor esegue un controllo (HTTP / ping / port / hardware tramite l'agente Capture)
|
||||
2. Il risultato viene memorizzato (successo/errore + tempo di risposta)
|
||||
3. I risultati dei controlli recenti vengono valutati rispetto alla soglia di cambio stato configurata per il monitor
|
||||
4. Se la soglia di cambio stato del monitor viene raggiunta e lo stato attuale è diverso da quello precedente, lo stato del monitor cambia (es. `initializing`, `up`, `down`, `breached`)
|
||||
5. Al cambio di stato: viene creato o risolto un incidente, a seconda dello stato attuale del monitor
|
||||
6. Le notifiche vengono attivate in base alla configurazione
|
||||
|
||||
<a id="screenshots"></a>
|
||||
## Screenshot
|
||||
|
||||
<p>
|
||||
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
|
||||
</p>
|
||||
<p>
|
||||
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<a id="tech-stack"></a>
|
||||
## Tech stack
|
||||
|
||||
- [ReactJs](https://react.dev/)
|
||||
- [MUI (framework React)](https://mui.com/)
|
||||
- [Node.js](https://nodejs.org/en)
|
||||
- [MongoDB](https://mongodb.com)
|
||||
- [Recharts](https://recharts.org)
|
||||
- Tanti altri componenti open source!
|
||||
|
||||
<a id="a-few-links"></a>
|
||||
## Alcuni link
|
||||
|
||||
- Se vuoi supportarci, valuta di lasciare una ⭐ e di cliccare su "watch".
|
||||
- Hai una domanda o un suggerimento per la roadmap o le funzionalità? Dai un'occhiata al nostro [canale Discord](https://discord.gg/NAb6H3UTjK) o al forum [Discussions](https://github.com/bluewave-labs/checkmate/discussions).
|
||||
- Vuoi ricevere una notifica quando esce una nuova release? Usa [Newreleases](https://newreleases.io/), un servizio gratuito per tenere traccia delle release.
|
||||
- Guarda un [video di installazione e utilizzo](https://www.youtube.com/watch?v=GfFOc0xHIwY) di Checkmate
|
||||
|
||||
<a id="contributing"></a>
|
||||
## Come contribuire
|
||||
|
||||
Siamo [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) e [Karen](https://github.com/karenvicent), e aiutiamo privati e aziende a monitorare la propria infrastruttura e i propri server.
|
||||
|
||||
Siamo orgogliosi di costruire legami solidi con i contributori a ogni livello. Pur essendo un progetto giovane, Checkmate ha già raccolto oltre 7000 stelle e attirato più di 90 contributori da tutto il mondo.
|
||||
|
||||
Il nostro repository ha ricevuto una stella da dipendenti di **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**, quindi non trattenerti: unisciti a noi, contribuisci e impara con noi!
|
||||
|
||||
Ecco come puoi contribuire:
|
||||
|
||||
0. Metti una stella a questo repository :)
|
||||
1. Consulta la [guida per i contributori](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Chi è alle prime armi è incoraggiato a controllare il tag `good-first-issue`.
|
||||
2. Leggi una descrizione dettagliata della struttura di [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) se vuoi approfondire l'architettura.
|
||||
3. Apri una issue se pensi di aver trovato un bug.
|
||||
4. Se sei un nuovo arrivato, dai un'occhiata alle good-first-issue.
|
||||
5. Apri una pull request per aggiungere nuove funzionalità, apportare miglioramenti dell'esperienza d'uso o correggere bug.
|
||||
6. Dai un'occhiata a questa guida interattiva alla codebase di `Checkmate` su CodeCanvas [qui](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Per perfezionare le simulazioni di flusso dati esistenti o crearne di nuove, segui il breve tutorial [qui](https://docs.code-canvas.com/updating-diagram).
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
</a>
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
🇪🇸 <a href="README.es.md">Español</a> |
|
||||
🇫🇮 <a href="README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="README.ru.md">Русский</a> |
|
||||
|
||||
Loaded 100 of 434 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user