fix: harden Nexus runtime health and rollback
This commit is contained in:
+2
-2
@@ -19,7 +19,7 @@ JWT_AUDIENCE=nexus-web
|
|||||||
BOOTSTRAP_OWNER_EMAIL=***
|
BOOTSTRAP_OWNER_EMAIL=***
|
||||||
|
|
||||||
# ── OpenClaw Integration ────────────────────────────────
|
# ── OpenClaw Integration ────────────────────────────────
|
||||||
# Base URL of the OpenClaw gateway (host.docker.internal from inside container)
|
# Internal Docker-DNS URL of the OpenClaw gateway
|
||||||
OPENCLAW_BASE_URL=http://host.docker.internal:18789
|
OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789
|
||||||
OPENCLAW_GATEWAY_TOKEN=***
|
OPENCLAW_GATEWAY_TOKEN=***
|
||||||
OPENCLAW_GATEWAY_PASSWORD=***
|
OPENCLAW_GATEWAY_PASSWORD=***
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ JWT_KEY=${ENV_JWT_KEY}
|
|||||||
JWT_ISSUER=nexus
|
JWT_ISSUER=nexus
|
||||||
JWT_AUDIENCE=nexus-web
|
JWT_AUDIENCE=nexus-web
|
||||||
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
|
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
|
||||||
OPENCLAW_BASE_URL=http://host.docker.internal:18789
|
OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789
|
||||||
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN:-}
|
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN:-}
|
||||||
OPENCLAW_GATEWAY_PASSWORD=
|
OPENCLAW_GATEWAY_PASSWORD=
|
||||||
NEXUS_VERSION=${VERSION}
|
NEXUS_VERSION=${VERSION}
|
||||||
@@ -140,9 +140,15 @@ echo "Checking live health"
|
|||||||
retry=0
|
retry=0
|
||||||
while [ "$retry" -lt 6 ]; do
|
while [ "$retry" -lt 6 ]; do
|
||||||
retry=$((retry + 1))
|
retry=$((retry + 1))
|
||||||
if curl -fsS --max-time 10 "$BASE_URL/health" >/dev/null; then
|
health_body="$(curl -fsS --max-time 10 "$BASE_URL/health" 2>/dev/null || true)"
|
||||||
|
case "$health_body" in
|
||||||
|
'{"status":"Healthy"'*)
|
||||||
echo "Health check passed"
|
echo "Health check passed"
|
||||||
break
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ -n "$health_body" ]; then
|
||||||
|
echo "Health endpoint is reachable but not healthy: $health_body" >&2
|
||||||
fi
|
fi
|
||||||
if [ "$retry" -eq 6 ]; then
|
if [ "$retry" -eq 6 ]; then
|
||||||
echo "Health check failed" >&2
|
echo "Health check failed" >&2
|
||||||
|
|||||||
@@ -112,9 +112,11 @@ jobs:
|
|||||||
JWT_ISSUER=nexus
|
JWT_ISSUER=nexus
|
||||||
JWT_AUDIENCE=nexus-web
|
JWT_AUDIENCE=nexus-web
|
||||||
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
|
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
|
||||||
OPENCLAW_BASE_URL=http://host.docker.internal:18789
|
OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789
|
||||||
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
|
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
|
||||||
OPENCLAW_GATEWAY_PASSWORD=
|
OPENCLAW_GATEWAY_PASSWORD=
|
||||||
|
NEXUS_VERSION=$(tr -d '[:space:]' < VERSION)
|
||||||
|
NEXUS_GIT_SHA=$(git rev-parse HEAD)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 600 "${ENV_TMPFILE}"
|
chmod 600 "${ENV_TMPFILE}"
|
||||||
@@ -127,18 +129,38 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
docker run --rm \
|
git archive --format=tar HEAD | docker run --rm -i \
|
||||||
-v "${{ gitea.workspace }}:/src:ro" \
|
|
||||||
-v "${DEPLOY_PATH}:/dest" \
|
-v "${DEPLOY_PATH}:/dest" \
|
||||||
alpine:latest \
|
alpine:latest \
|
||||||
sh -c "
|
sh -c '
|
||||||
cd /src && \
|
set -eu
|
||||||
find . -mindepth 1 -maxdepth 1 \
|
dest_owner="$(stat -c "%u:%g" /dest)"
|
||||||
! -name .git \
|
mkdir -p /src-snapshot
|
||||||
-exec cp -r {} /dest/ \; && \
|
tar -xf - -C /src-snapshot
|
||||||
DEST_OWNER=\$(stat -c '%u:%g' /dest) && \
|
|
||||||
chown -R \"\$DEST_OWNER\" /dest
|
is_protected_path() {
|
||||||
"
|
case "$1" in
|
||||||
|
./.git|./.env|./.env.*|./data|./logs|./backups|./tmp|./uploads|./storage)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cd /dest
|
||||||
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
||||||
|
if ! is_protected_path "$path"; then rm -rf "$path"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cd /src-snapshot
|
||||||
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
||||||
|
if ! is_protected_path "$path"; then cp -a "$path" /dest/; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
chown -R "$dest_owner" /dest
|
||||||
|
'
|
||||||
|
|
||||||
echo "✅ Rollback code (${{ inputs.target_tag }}) synced to ${DEPLOY_PATH}"
|
echo "✅ Rollback code (${{ inputs.target_tag }}) synced to ${DEPLOY_PATH}"
|
||||||
|
|
||||||
@@ -151,16 +173,18 @@ jobs:
|
|||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "${DEPLOY_PATH}:/workspace/nexus" \
|
-v "${DEPLOY_PATH}:/workspace/nexus" \
|
||||||
-v "/tmp:/tmp-host:ro" \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-w /workspace/nexus \
|
-w /workspace/nexus \
|
||||||
|
-i \
|
||||||
docker:cli \
|
docker:cli \
|
||||||
sh -c "
|
sh -c '
|
||||||
set -e
|
set -eu
|
||||||
echo '🔙 Rolling back to ${{ inputs.target_tag }}'
|
umask 077
|
||||||
docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") build --no-cache
|
cat > /tmp/nexus-rollback-env
|
||||||
docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") up -d --wait --force-recreate
|
trap '\''rm -f /tmp/nexus-rollback-env'\'' EXIT INT TERM
|
||||||
"
|
docker compose --env-file /tmp/nexus-rollback-env build --no-cache
|
||||||
|
docker compose --env-file /tmp/nexus-rollback-env up -d --wait --force-recreate
|
||||||
|
' < "${ENV_TMPFILE}"
|
||||||
|
|
||||||
echo "✅ Rollback redeploy completed"
|
echo "✅ Rollback redeploy completed"
|
||||||
|
|
||||||
@@ -186,11 +210,14 @@ jobs:
|
|||||||
WAIT=1
|
WAIT=1
|
||||||
while [ $RETRY -lt $MAX ]; do
|
while [ $RETRY -lt $MAX ]; do
|
||||||
RETRY=$((RETRY + 1))
|
RETRY=$((RETRY + 1))
|
||||||
if curl -sf --max-time 10 https://nexus.noveria.net/health; then
|
HEALTH_BODY=$(curl -sf --max-time 10 https://nexus.noveria.net/health || true)
|
||||||
echo ""
|
case "$HEALTH_BODY" in
|
||||||
|
'{"status":"Healthy"'*)
|
||||||
echo "✅ Health check passed (attempt $RETRY/$MAX)"
|
echo "✅ Health check passed (attempt $RETRY/$MAX)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
;;
|
||||||
|
esac
|
||||||
|
[ -n "$HEALTH_BODY" ] && echo "⚠️ Health endpoint is degraded: $HEALTH_BODY"
|
||||||
echo "⏳ Attempt $RETRY/$MAX failed, waiting ${WAIT}s..."
|
echo "⏳ Attempt $RETRY/$MAX failed, waiting ${WAIT}s..."
|
||||||
sleep $WAIT
|
sleep $WAIT
|
||||||
NEXT=$((WAIT + RETRY))
|
NEXT=$((WAIT + RETRY))
|
||||||
|
|||||||
+12
-3
@@ -2,6 +2,15 @@ name: nexus
|
|||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
|
# WAL-Archivierung bleibt deaktiviert, bis ein verwaltetes Off-Server-Ziel
|
||||||
|
# mit Retention und Restore-Test existiert. Ein lokales Endlosarchiv ist
|
||||||
|
# kein Backup und kann bei Fehlern pg_wal ungebremst wachsen lassen.
|
||||||
|
command:
|
||||||
|
- postgres
|
||||||
|
- -c
|
||||||
|
- archive_mode=off
|
||||||
|
- -c
|
||||||
|
- archive_command=
|
||||||
restart: always
|
restart: always
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -50,7 +59,7 @@ services:
|
|||||||
Jwt__Audience: ${JWT_AUDIENCE:-nexus-web}
|
Jwt__Audience: ${JWT_AUDIENCE:-nexus-web}
|
||||||
Bootstrap__OwnerEmail: ${BOOTSTRAP_OWNER_EMAIL:?Set BOOTSTRAP_OWNER_EMAIL in .env}
|
Bootstrap__OwnerEmail: ${BOOTSTRAP_OWNER_EMAIL:?Set BOOTSTRAP_OWNER_EMAIL in .env}
|
||||||
# Initial owner password is generated once at first seed and then lives only in the DB.
|
# Initial owner password is generated once at first seed and then lives only in the DB.
|
||||||
Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789}
|
Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://openclaw-gateway-bao:18789}
|
||||||
Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-}
|
Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-}
|
||||||
Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-}
|
Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-}
|
||||||
Admin__ResetToken: ${Admin__ResetToken:-}
|
Admin__ResetToken: ${Admin__ResetToken:-}
|
||||||
@@ -59,7 +68,7 @@ services:
|
|||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
restart: true
|
restart: true
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1"]
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1"]
|
||||||
@@ -106,7 +115,7 @@ services:
|
|||||||
- "127.0.0.1:18880:80"
|
- "127.0.0.1:18880:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
api:
|
api:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
restart: true
|
restart: true
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"]
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ Ansatz. Das Backend fungiert bereits als sichere Schicht zwischen allen Akteuren
|
|||||||
│ │
|
│ │
|
||||||
│ ALLE Gateway-Calls → Authorization: Bearer <Gateway-Password> │
|
│ ALLE Gateway-Calls → Authorization: Bearer <Gateway-Password> │
|
||||||
└──────────────┬──────────────────────────────┬────────────────────┘
|
└──────────────┬──────────────────────────────┬────────────────────┘
|
||||||
│ host.docker.internal:18789 │
|
│ openclaw-gateway-bao:18789 │
|
||||||
│ (Gateway loopback/lan) │
|
│ (internes Docker-DNS) │
|
||||||
▼ │
|
▼ │
|
||||||
┌──────────────────────────────┐ │
|
┌──────────────────────────────┐ │
|
||||||
│ OpenClaw Gateway Container │ │
|
│ OpenClaw Gateway Container │ │
|
||||||
@@ -199,7 +199,7 @@ Ebene 4: X-Agent-Id Header (Agent-Identität für Task-State-Enforcement)
|
|||||||
```
|
```
|
||||||
POST /api/v1/operations/snapshot
|
POST /api/v1/operations/snapshot
|
||||||
→ DashboardService → OpenClawGatewayClient.InvokeToolAsync()
|
→ DashboardService → OpenClawGatewayClient.InvokeToolAsync()
|
||||||
→ POST http://host.docker.internal:18789/tools/invoke
|
→ POST http://openclaw-gateway-bao:18789/tools/invoke
|
||||||
Authorization: Bearer <Gateway-Password>
|
Authorization: Bearer <Gateway-Password>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ POST /api/v1/operations/snapshot
|
|||||||
|
|
||||||
### 5.2 Docker-Netzwerk & Gateway-Bind
|
### 5.2 Docker-Netzwerk & Gateway-Bind
|
||||||
|
|
||||||
**Aktuelles Problem:**
|
**Aktueller Stand (2026-07-09):**
|
||||||
```
|
```
|
||||||
compose.yaml:
|
compose.yaml:
|
||||||
api:
|
api:
|
||||||
@@ -228,32 +228,27 @@ compose.yaml:
|
|||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
networks:
|
networks:
|
||||||
- nexus
|
- nexus
|
||||||
- openclaw_default ← API-Container ist im Gateway-Netzwerk
|
- openclaw_default
|
||||||
|
|
||||||
Gateway-Konfiguration:
|
Gateway-Konfiguration:
|
||||||
gateway.bind: "loopback" ← Bindet nur 127.0.0.1 IM GATEWAY-CONTAINER
|
gateway.bind: "lan"
|
||||||
|
|
||||||
|
Nexus-Konfiguration:
|
||||||
|
OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ergebnis:**
|
**Ergebnis:**
|
||||||
- `host.docker.internal:18789` funktioniert, weil `extra_hosts` auf den Docker-Host zeigt
|
- Nexus erreicht das Gateway direkt über Docker-DNS im gemeinsamen `openclaw_default`-Netz.
|
||||||
- ABER: Docker-Port-Forward (wenn vorhanden) sendet an Container-IP, nicht loopback
|
- Der Umweg über einen nicht veröffentlichten Host-Port entfällt.
|
||||||
- Die `openclaw_default` Netzwerk-Mitgliedschaft des API-Containers wird NICHT genutzt
|
- Der produktive Aggregat-Healthcheck prüft neben PostgreSQL auch die Runtime-Verbindung.
|
||||||
|
|
||||||
**Empfehlung (siehe gateway-api-research.md, Abschnitt 6):**
|
Der frühere Pfad `host.docker.internal:18789` war auf dem VPS nicht erreichbar und ist obsolet.
|
||||||
```json5
|
|
||||||
// openclaw.json
|
|
||||||
{
|
|
||||||
gateway: {
|
|
||||||
bind: "lan" // war "loopback"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternativ: API-Container über Gateway-Container-Namen ansprechen:
|
Produktive Einstellung:
|
||||||
```yaml
|
```yaml
|
||||||
Integrations__OpenClaw__BaseUrl: http://openclaw_gateway:18789
|
Integrations__OpenClaw__BaseUrl: http://openclaw-gateway-bao:18789
|
||||||
```
|
```
|
||||||
(Vorausgesetzt der Gateway-Container heißt `openclaw_gateway` und ist im `openclaw_default` Netzwerk)
|
Beide Container müssen Mitglied im `openclaw_default`-Netzwerk sein.
|
||||||
|
|
||||||
### 5.3 MCP-artige Integration: Bewertung
|
### 5.3 MCP-artige Integration: Bewertung
|
||||||
|
|
||||||
|
|||||||
@@ -290,30 +290,30 @@ The Nexus compose.yaml already includes the full integration infrastructure:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
api:
|
api:
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
environment:
|
environment:
|
||||||
Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789}
|
Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://openclaw-gateway-bao:18789}
|
||||||
Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-}
|
Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-}
|
||||||
Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-}
|
Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-}
|
||||||
|
networks:
|
||||||
|
- nexus
|
||||||
|
- openclaw_default
|
||||||
```
|
```
|
||||||
|
|
||||||
The API container:
|
The API container:
|
||||||
- Uses `host.docker.internal:18789` to reach the Gateway via the Docker host
|
- Uses Docker DNS (`openclaw-gateway-bao:18789`) in the shared `openclaw_default` network
|
||||||
- Has `extra_hosts` configured for `host.docker.internal`
|
- Does not depend on a published host port for the Gateway
|
||||||
- Reads token/password from `.env` via `OPENCLAW_GATEWAY_PASSWORD`
|
- Reads token/password from `.env` via `OPENCLAW_GATEWAY_PASSWORD`
|
||||||
|
|
||||||
### Known Issue: Gateway Bind = loopback
|
### Resolved Routing Issue (2026-07-09)
|
||||||
|
|
||||||
The Gateway binds to `127.0.0.1` (`gateway.bind: "loopback"`). This means it only listens inside the gateway container's loopback interface.
|
The old `host.docker.internal:18789` route was unreachable because no usable host port was published. The Gateway is now reached directly by its container DNS name.
|
||||||
|
|
||||||
| Scenario | Works? | Why |
|
| Scenario | Works? | Why |
|
||||||
|----------|--------|-----|
|
|----------|--------|-----|
|
||||||
| Gateway with `--network host` | ✅ Yes | Process sees host's 127.0.0.1 directly |
|
| `host.docker.internal:18789` | ❌ No | No reachable host listener on the VPS |
|
||||||
| Gateway with `-p 18789:18789` + loopback bind | ❌ No | Port forward sends to container IP, not loopback |
|
| `openclaw-gateway-bao:18789` in `openclaw_default` | ✅ Yes | Direct container-to-container routing via Docker DNS |
|
||||||
| Gateway with `-p 18789:18789` + lan bind | ✅ Yes | Listens on all interfaces including container IP |
|
|
||||||
|
|
||||||
**Fix**: Change `gateway.bind` from `"loopback"` to `"lan"` (binds `0.0.0.0`):
|
The Gateway must listen on its container interface (`gateway.bind: "lan"`):
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -325,8 +325,8 @@ The Gateway binds to `127.0.0.1` (`gateway.bind: "loopback"`). This means it onl
|
|||||||
|
|
||||||
**Test command (from Nexus API container):**
|
**Test command (from Nexus API container):**
|
||||||
```bash
|
```bash
|
||||||
curl -s http://host.docker.internal:18789/health
|
curl -s http://openclaw-gateway-bao:18789/
|
||||||
# Expected: 200 if gateway bind is lan/container IP is reachable
|
# Expected: HTTP 200 from inside nexus-api-1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Required .env Vars for Nexus
|
### Required .env Vars for Nexus
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Runtime und Routing
|
# Runtime und Routing
|
||||||
|
|
||||||
> Letzte Aktualisierung: 2026-06-16
|
> Letzte Aktualisierung: 2026-07-09
|
||||||
|
|
||||||
## Aktive Modelle (7 von 8 konfiguriert)
|
## Aktive Modelle (7 von 8 konfiguriert)
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
- Einzige aktive Integration: `OpenClawRuntime` über `IAgentRuntime`
|
- Einzige aktive Integration: `OpenClawRuntime` über `IAgentRuntime`
|
||||||
- Model-Routing läuft zentral über OpenClaw Gateway (kein direct provider routing)
|
- Model-Routing läuft zentral über OpenClaw Gateway (kein direct provider routing)
|
||||||
- API kommuniziert via `host.docker.internal:18789` (Gateway loopback — wird über `openclaw_default` Netzwerk gefixt)
|
- API kommuniziert über Docker-DNS via `http://openclaw-gateway-bao:18789` im gemeinsamen `openclaw_default`-Netzwerk.
|
||||||
|
- Der frühere Pfad `host.docker.internal:18789` ist auf dem VPS nicht erreichbar und wurde entfernt.
|
||||||
- **Achtung:** `[AllowAnonymous]` auf `/tasks/board` und `/tasks/reset-stale` muss durch ApiKey-Auth ersetzt werden (siehe [Architektur-Review](../docs/architecture-board-first-orchestration.md#61-kritisch--allowanonymous-auf-board-endpunkten))
|
- **Achtung:** `[AllowAnonymous]` auf `/tasks/board` und `/tasks/reset-stale` muss durch ApiKey-Auth ersetzt werden (siehe [Architektur-Review](../docs/architecture-board-first-orchestration.md#61-kritisch--allowanonymous-auf-board-endpunkten))
|
||||||
- Vollständige Architektur- und Sicherheitsanalyse: [architecture-board-first-orchestration.md](../docs/architecture-board-first-orchestration.md)
|
- Vollständige Architektur- und Sicherheitsanalyse: [architecture-board-first-orchestration.md](../docs/architecture-board-first-orchestration.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user