# NODE.DC Synology deploy

Эта папка фиксирует текущий воспроизводимый NAS-deploy для `nodedc-platform` на Synology RS1221RP+.

## Правила

- Не выполнять `docker stop`, `docker restart`, `docker compose down`, `docker system prune` для старых проектов.
- Новый compose project: `nodedc-platform`.
- Новая папка на NAS: `/volume1/docker/nodedc-platform`.
- Внутренний HTTP edge использует `18080`, AI Workspace Hub — `18081`, Tasker upstream — `18090`, BIM Viewer upstream — `18100`, Ops Agents Gateway upstream — `18190`.
- Старые порты `9000` и `5678` заняты старым `nodedc-demo` и не используются.

## Deploy runner

`nodedc-deploy` is managed outside the platform runtime tree. The synced install candidate lives in:

```text
/volume1/docker/nodedc-deploy/runner-install/nodedc-deploy
```

The live root-owned runner is:

```text
/usr/local/sbin/nodedc-deploy
```

`deploy-current.sh` syncs the runner candidate into `runner-install`. Promoting it to live is explicit:

```bash
sudo install -o root -g root -m 0755 \
  /volume1/docker/nodedc-deploy/runner-install/nodedc-deploy \
  /usr/local/sbin/nodedc-deploy
sudo /usr/local/sbin/nodedc-deploy verify-install
```

## Текущие внешние домены

```text
https://id.nodedc.ru  -> Authentik
https://hub.nodedc.ru -> Launcher / Hub
https://ops.nodedc.ru -> Tasker / Operational Core
https://bim.nodedc.tech -> BIM Viewer
https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP
https://ai-hub.nodedc.ru -> AI Workspace Hub / WebSocket relay
```

`id.nodedc.ru` is the user-facing OIDC/login host. Authentik Admin is intentionally not exposed through this public host; `/if/admin/*` returns `404` there.

`ai-hub.nodedc.ru` is intentionally routed by Synology DSM Reverse Proxy directly to `172.22.0.222:18081`, because this endpoint must carry WebSocket Upgrade traffic for remote Codex agents.

В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`.

`bim.nodedc.tech` can either be routed by DSM directly to the BIM host port `172.22.0.222:18100`, or through the platform HTTP edge `172.22.0.222:18080`. When it goes through the platform edge, `BIM_DOMAIN=bim.nodedc.tech` and `SYNOLOGY_BIM_VIEWER_UPSTREAM=host.docker.internal:18100` keep the same target service.

## AI Workspace Hub / Assistant contract

`ai-workspace-hub` remains a thin public WebSocket relay. It is the only AI Workspace service exposed through DSM/Nginx as `https://ai-hub.nodedc.ru`.

`ai-workspace-assistant` is an internal platform service for per-user executors, selected device, shared threads and installer generation. It must not be exposed as a public route. Launcher, Engine and Tasker use it through Docker networking:

```env
NODEDC_AI_WORKSPACE_ASSISTANT_URL=http://ai-workspace-assistant:18082
PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL=http://ai-workspace-assistant:18082
```

Installer generation must always point remote Codex workers to the deployed Hub:

```env
AI_WORKSPACE_HUB_PUBLIC_URL=wss://ai-hub.nodedc.ru/api/ai-workspace/hub
AI_WORKSPACE_HUB_INTERNAL_URL=https://ai-hub.nodedc.ru
AI_WORKSPACE_HUB_FALLBACK_URLS=
```

Server-side Hub API calls, including executor status checks, use `AI_WORKSPACE_HUB_INTERNAL_URL` and require a token accepted by Hub: `AI_WORKSPACE_HUB_TOKEN`, `NDC_AI_WORKSPACE_HUB_TOKEN`, or the shared `NODEDC_INTERNAL_ACCESS_TOKEN` where that token is intentionally common across platform services.

Local NDC SEO Mode development uses this same server-side Hub class through `SEO_AI_WORKSPACE_PROFILE=deployed-hub`, `SEO_AI_WORKSPACE_CONTROL_URL=https://ai-hub.nodedc.ru`, and a Hub-accepted `SEO_AI_WORKSPACE_CONTROL_TOKEN`. The Hub only exposes an Assistant-compatible allowlist proxy for setup/probe/thread dispatch calls behind bearer-token auth; it does not expose unrestricted Assistant, product apps, Authentik, Engine, Ops, or Tasker to the remote Codex worker.

AI Workspace Assistant also calls product entitlement adapters before every Codex run. Ops uses the Agent Gateway internal endpoint; the token must match `NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN` from the Ops Agent Gateway deployment:

```env
AI_WORKSPACE_OPS_ENTITLEMENT_URL=http://172.22.0.222:18190/api/internal/v1/ai-workspace/entitlements
AI_WORKSPACE_OPS_ENTITLEMENT_TOKEN=<same value as Ops NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN>
AI_WORKSPACE_OPS_ENTITLEMENT_REQUIRED=false
```

## Локальные домены для первичной проверки

На Mac для первичной проверки добавить в `/etc/hosts`:

```text
172.22.0.222 auth.nas.nodedc
172.22.0.222 auth-admin.nas.nodedc
172.22.0.222 launcher.nas.nodedc
172.22.0.222 task.nas.nodedc
```

Первичные URL:

```text
http://auth.nas.nodedc:18080
http://auth-admin.nas.nodedc:18080/if/admin/
http://172.22.0.222:18080/if/admin/
http://launcher.nas.nodedc:18080
http://task.nas.nodedc:18080
http://task.nas.nodedc:18090
```

`auth-admin.nas.nodedc` is the technical Authentik Admin entrypoint. `172.22.0.222:18080` is a local IP fallback for workstations without `auth-admin.nas.nodedc` DNS. Both keep Authentik access separate from the public `id.nodedc.ru` login entrypoint and do not load NODE.DC auth-flow CSS.

## Что входит

- `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher, Notification Core, AI Workspace Hub и Caddy edge.
- `Caddyfile.http` маршрутизирует локальные `auth/auth-admin/launcher/task.nas.nodedc`, IP fallback `172.22.0.222` для Authentik Admin и внешние `id/hub/ops.nodedc.ru`.
- `deploy-current.sh` синхронизирует compose, Caddyfile, Notification Core source, AI Workspace Hub source и опционально Launcher/BIM source в NAS mount. Authentik templates синхронизируются только при явном `SYNC_AUTHENTIK_TEMPLATES=1`.
- `backup-current.sh` делает snapshot Launcher runtime/uploads/Auth templates/config, BIM Viewer `server/data` и готовит команду `pg_dump` для Authentik Postgres.
- Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`.
- Ops Agents Gateway поднимается отдельным compose из `NODEDC_TASKMANAGER_CODEXAPI/docker-compose.synology.yml` на `172.22.0.222:18190`; Synology reverse proxy должен вести `ops-agents.nodedc.ru` на этот порт, а не на `18090`.

## Внутренний Authentik API для Launcher

Launcher подключен и к platform identity-сети, и к engine-сети. В engine-сети тоже может быть сервис с DNS-именем `authentik-server`, поэтому это имя нельзя использовать для `NODEDC_AUTHENTIK_BASE_URL`: Docker DNS может отдать не тот Authentik, и platform API-token будет получать `403 Token invalid/expired`.

Для platform Authentik зафиксирован отдельный alias:

```text
http://nodedc-platform-authentik-server:9000
```

Live `/volume1/docker/nodedc-platform/platform/.env.synology` должен содержать:

```env
NODEDC_AUTHENTIK_BASE_URL=http://nodedc-platform-authentik-server:9000
AUTHENTIK_BASE_URL=http://nodedc-platform-authentik-server:9000
NODEDC_AUTHENTIK_SERVICE_TOKEN=<server Authentik API token>
AUTHENTIK_SERVICE_TOKEN=<same token>
```

Быстрая проверка из launcher-контейнера:

```bash
sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc '
echo "$NODEDC_AUTHENTIK_BASE_URL"
getent hosts nodedc-platform-authentik-server
for attempt in 1 2 3 4 5 6 7 8 9 10; do
  wget -qSO- \
    --header "Authorization: Bearer $NODEDC_AUTHENTIK_SERVICE_TOKEN" \
    "$NODEDC_AUTHENTIK_BASE_URL/api/v3/core/groups/?search=nodedc_admin" \
    2>&1 | head -n 25 && exit 0
  echo "authentik-api-not-ready attempt=$attempt"
  sleep 10
done
exit 1
'
```

Ожидаемый результат: `HTTP/1.1 200 OK`.

После изменения platform Authentik alias пересоздавать нужно `authentik-server`, `authentik-worker` и `launcher`. Один `authentik-server` может временно отдавать `503 Service Unavailable`, пока worker и bootstrap не готовы.

## Синхронизация текущего состояния

С Mac, при смонтированном `/Volumes/docker`:

```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
NAS_ROOT=/Volumes/docker/nodedc-platform \
LAUNCHER_REPO=/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher \
BIM_REPO=/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_BIM_VIEWER \
TASKER_REPO=/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER \
TASKER_CHANGED_BASE=533f8c6 \
GATEWAY_REPO=/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI \
./infra/synology/deploy-current.sh
```

Скрипт не запускает Docker сам: на NAS `sudo` интерактивный, поэтому команды применения печатаются в конце.

По умолчанию sync source-копий на SMB/NAS не сохраняет owner/group/perms/times. Для Docker build важен контент, а macOS/Synology metadata может падать на `utimensat Operation timed out`. Если metadata действительно нужно сохранить для отдельного ручного случая, запускать с `RSYNC_PRESERVE_METADATA=1`.

Что синхронизируется:

- Platform compose/Caddy.
- Notification Core source в `/volume1/docker/nodedc-platform/platform/notification-core`.
- AI Workspace Hub source в `/volume1/docker/nodedc-platform/platform/ai-workspace-hub`.
- Authentik templates только при `SYNC_AUTHENTIK_TEMPLATES=1`; по умолчанию они не трогаются, чтобы лёгкий Hub deploy не уносил экспериментальную тему/брендинг в prod.
- Launcher source в `/volume1/docker/nodedc-platform/launcher/source`.
- BIM Viewer source в `/volume1/docker/nodedc-platform/bim-viewer/source`; `server/data` не синхронизируется и остаётся live runtime-хранилищем моделей, shares, comments и refs.
- Tasker `plane-app/docker-compose.yaml` и, если задан `TASKER_CHANGED_BASE`, только изменённые source-файлы из диапазона `TASKER_CHANGED_BASE..HEAD`.
- Ops Agents Gateway source в `/volume1/docker/nodedc-platform/ops-agents`.

Полный sync Tasker source по SMB тяжёлый для Plane fork. Использовать его только осознанно:

```bash
TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh
```

Секретные runtime env-файлы не перетираются:

- `/volume1/docker/nodedc-platform/platform/.env.synology`
- `/volume1/docker/nodedc-platform/bim-viewer/source/.env`
- `/volume1/docker/nodedc-platform/tasker/plane-app/.env.synology`
- `/volume1/docker/nodedc-platform/ops-agents/.env`

Если emergency-fix был сделан прямо на Synology в этих env-файлах, перенести sanitized-значение в `.env.synology.example`/docs, а секрет оставить только в live env.

### Gelios: frozen legacy compatibility contour

`gelios-postgres` и `gelios-gateway` — уже существующий self-hosted контур с
собственным persistent Timescale volume. Он остаётся полностью воспроизводимым
в source/deploy, но не развивается и не используется как шаблон для новых
провайдеров. Его удаление или миграция требуют отдельного подтверждённого плана;
обычный Platform/EDP deploy не удаляет volume и не изменяет старый workflow.

`GELIOS_UNIT_SCOPE=all` означает **все текущие и будущие units только одной уже
настроенной пары `GELIOS_TENANT_ID` + `GELIOS_CONNECTION_ID`**. Это не wildcard
на другие tenants или providers. Provider credential остаётся только в Engine.
Пропавший из очередного ответа unit не удаляется: его stable provider ID и
`last_seen_at` сохраняются; видимость на карте — отдельная логика витрины.

Перед узким legacy apply включить политику на Synology (скрипт создаёт backup и
не выводит секреты):

```bash
sudo bash /volume1/docker/nodedc-deploy/inbox/prepare-gelios-all-units-env.sh
```

В plan такого Platform-артефакта должны быть только `gelios-postgres` и
`gelios-gateway`; общий compose в узкий архив не входит.

## EDP publish grant: platform-owned credential

`NDC Data Product Writer API` — историческое имя native credential type для
внутреннего scoped EDP publish grant. Это не Gelios access/refresh token и не
право записи в Gelios.

Целевой путь полностью служебный: control plane NDC по разрешённому connection
profile генерирует capability внутри native NDC L2 Credentials, передаёт EDP
только digest для scoped binding и возвращает MCP только opaque compatible
reference/status.
Пользователь не читает, не копирует и не вводит этот внутренний секрет.

EDP уже имеет localhost-only digest-only managed endpoint. Managed endpoint и
legacy plaintext provisioning имеют раздельные флаги и по умолчанию выключены.
Runner-owned provisioner bearer относится только к legacy plaintext routes и
на managed ensure/revoke намеренно не действует. Managed caller contract —
Ed25519-signed Engine service request с exact audience, method, request target,
raw-body SHA-256, timestamp и одноразовым nonce; EDP хранит только public key и
fail-closed replay cache. Ни bearer, ни signing private key нельзя открывать
workflow, MCP или переиспользовать как shared credential. В source-кандидате
Engine добавлен отдельный server-derived plan/apply для exact пары
`ndcDataProductWriterApi` + custom Publish node; он не расширяет generic HTTP
safe-ref policy и не принимает provider/scope/credential identity, capability,
generation или service URL от MCP. До отдельного Engine deploy и runtime proof
текущий deployed MCP этого контракта не имеет. Поэтому отсутствие
совместимого reference — платформенный runtime gap, а не действие пользователя.
Ручной перенос capability из отдельно включаемого legacy endpoint через root/UI допустим только как аварийная
диагностика self-hosted контура; он не является acceptance-путём и не должен
закрепляться в пользовательской автоматизации.

Compose монтирует read-only trust directory
`/volume1/docker/nodedc-platform/trust/engine-managed-provisioner` в EDP; в нём
ожидается один regular, non-symlink файл `public-key.pem` с Ed25519 SPKI public
key. Файл не должен быть group/world-writable и должен читаться runtime UID
`11006`. Private key в этот каталог, Platform artifact, `.env` или EDP
контейнер не попадает. Canonical deploy runner теперь атомарно создаёт или
проверяет matching Ed25519 pair перед relevant Engine/EDP Compose apply: private
key остаётся `root:root 0400`, public trust — `root:11006 0440`; public-only
crash state, mismatch, symlink и неверный тип ключа отклоняются до запуска.
`EXTERNAL_DATA_PLANE_MANAGED_PROVISIONING_ENABLED` остаётся `false` на staging.
В отдельно подтверждённом activation window его переводят в `true`
непосредственно перед Platform EDP artifact; runner принимает deploy только
если `/healthz` явно вернул managed status `enabled`. Старый Engine в этот
момент ещё не имеет signer mount, поэтому usable caller появляется лишь после
следующего отдельно принятого Engine artifact.
EDP application artifact пересобирает и force-recreate только
`external-data-plane`. Уже работающий `external-data-plane-postgres` и его
Timescale volume runner не выбирает и не перезапускает; healthy database —
обязательная deploy prerequisite.
Canonical service/key/audience заданы в `.env.synology.example`; wire headers и
canonical signing payload описаны в `packages/external-provider-contract/README.md`.

До появления opaque compatible reference и успешного manual publish proof
Schedule Trigger не включать. Frozen legacy workflow и его credentials этот
процесс не изменяет.

## AI Hub relay-only deploy

Для локального тестирования с внешней Codex-машиной нельзя деплоить Launcher, Engine, Ops, Authentik или Tasker. Единственная допустимая удалённая точка в этой схеме — `ai-workspace-hub`, потому что удалённый worker должен иметь публичный WebSocket/HTTP relay.

С Mac, при смонтированном `/Volumes/docker`, синхронизировать только Hub source, compose и узкий apply-script:

```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
NAS_ROOT=/Volumes/docker/nodedc-platform ./infra/synology/sync-ai-hub-relay.sh
```

На Synology применить только `ai-workspace-hub`:

```bash
ssh dctouch@100.109.216.21 'cd /volume1/docker/nodedc-platform/platform && sudo bash apply-ai-hub-relay.sh'
```

Этот путь намеренно делает только:

- build `nodedc/ai-workspace-hub:local`;
- `docker compose up -d --no-deps --force-recreate ai-workspace-hub`;
- container health check;
- проверку `/api/ai-workspace/hub/v1/assistant-relays/<relayId>/poll`;
- public route check через `https://ai-hub.nodedc.ru`.

Он не меняет `.env.synology`, не синхронизирует Launcher/Tasker/Ops source, не пересоздаёт Authentik, reverse-proxy, базы, storage или product containers.

После успешного relay-only deploy локальная цепочка для теста должна быть такой:

```text
local Launcher/Engine/Ops/Auth/Assistant
  -> deployed AI Hub relay: https://ai-hub.nodedc.ru
  -> remote Codex worker
  -> deployed AI Hub relay
  -> local Assistant relay poll
  -> local product services
```

## Лёгкое обновление Hub / Launcher

Для правок только в Launcher/BFF не пересоздавать Authentik, reverse-proxy, Tasker и Ops Agents:

```bash
cd /volume1/docker/nodedc-platform/launcher/source
sudo /usr/local/bin/docker build --no-cache -t nodedc/launcher:local .

cd /volume1/docker/nodedc-platform/platform
sudo /usr/local/bin/docker compose \
  --env-file /volume1/docker/nodedc-platform/platform/.env.synology \
  -f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
  up -d --force-recreate --no-deps ai-workspace-hub launcher
```

После такого deploy проверить `healthz`, запись в launcher storage/uploads и сценарий пользователя без аппрува: сохранение аватара не должно показывать экран `Заявка ожидает подтверждения`. Дополнительно проверить, что live bundle больше не содержит старый pending gate:

```bash
launcher_asset="$(
  curl -k -sS --compressed -H 'Accept: text/html' https://hub.nodedc.ru/ \
    | grep -aoE 'index-[A-Za-z0-9_-]+\.js' \
    | head -n 1
)"
test -n "$launcher_asset"
if curl -k -sS --compressed "https://hub.nodedc.ru/assets/${launcher_asset}" \
  | grep -aq 'Заявка ожидает подтверждения'; then
  echo 'old pending gate still present'
  exit 1
fi
echo 'launcher-pending-gate-ok'
```

## BIM Viewer deploy

`bim.nodedc.tech` на Synology можно вести напрямую через DSM Reverse Proxy на `172.22.0.222:18100`. В этом режиме Platform Caddy не участвует в публичном BIM-трафике, но `Caddyfile.http` всё равно содержит BIM route как запасной вариант для маршрута через `172.22.0.222:18080`.

Canonical BIM deploy now uses `nodedc-deploy` with `component=bim-viewer`; do not deploy BIM by manually running `docker compose up` from the source directory as the normal path.

Live BIM root:

```text
/volume1/docker/nodedc-platform/bim-viewer/source
```

Live env stays outside artifacts:

```text
/volume1/docker/nodedc-platform/bim-viewer/source/.env
```

Before the first canonical apply, prepare `.env` from `.env.synology.example` and verify that `NODEDC_INTERNAL_ACCESS_TOKEN` matches `platform/.env.synology`. After that, `.env` must not be included in deploy artifacts.

Canonical deploy commands:

```bash
sudo /usr/local/sbin/nodedc-deploy verify-install
sudo /usr/local/sbin/nodedc-deploy plan /volume1/docker/nodedc-deploy/inbox/<bim-artifact>.tgz
sudo /usr/local/sbin/nodedc-deploy apply /volume1/docker/nodedc-deploy/inbox/<bim-artifact>.tgz
```

The `bim-viewer` runner component:

- applies payload under `/volume1/docker/nodedc-platform/bim-viewer/source`;
- rejects `.env`, `server/data`, upload/model/comment/share runtime storage, `node_modules`, `.git`, shell hooks and backup files;
- prepares required live runtime directories under `server/data` before compose;
- builds `nodedc/bim-converter:local` from `converter/Dockerfile`;
- recreates only `ndc-beam-viewer` and `nodedc-bim-converter`;
- healthchecks `http://127.0.0.1:18100/api/auth/session`.

Минимальная проверка после apply:

```bash
curl -k -fsS http://127.0.0.1:18100/api/auth/session
curl -k -sS -o /dev/null -w '%{http_code}\n' https://bim.nodedc.tech/
```

Ожидаемо: `/api/auth/session` возвращает `authRequired: true`, а прямой public root без BIM-сессии отдаёт `302` в Launcher login/launch. Public share `/share/<token>` должен открываться без авторизации и без toolbar.

## Tasker / OPS BIM iframe rebuild

OPS web bundle получает BIM URL на build-time через `VITE_BEAM_VIEWER_BASE_URL` и `VITE_BEAM_API_BASE_URL`. После смены домена пересобрать только web image:

```bash
cd /volume1/docker/nodedc-platform/tasker/plane-src
VITE_BEAM_VIEWER_BASE_URL=https://bim.nodedc.tech \
VITE_BEAM_API_BASE_URL=https://bim.nodedc.tech \
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 \
sh rebuild-nas-legacy.sh
```

Это пересоздаёт только `web` и не трогает PostgreSQL/MinIO volumes.

## Authentik Admin и Brand CSS

NODE.DC auth-flow CSS must stay template-scoped. Do not store it in Authentik `Brand.branding_custom_css`: Authentik passes that CSS into Admin/User web component runtime and breaks native controls.

After syncing Authentik templates to NAS, recreate `reverse-proxy authentik-server authentik-worker launcher`, then clear existing global Brand CSS in the live DB:

```bash
cd /volume1/docker/nodedc-platform/platform
sudo bash clear-authentik-brand-css.sh
```

Verify:

```bash
id_admin_status="$(
  curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/
)"
if [[ "$id_admin_status" != "404" ]]; then
  echo "public id admin is not closed: status=${id_admin_status}"
  exit 1
fi
echo 'public-id-admin-closed-ok'

auth_admin_page="$(
  curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/admin/
)"
printf '%s' "$auth_admin_page" \
  | grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'

auth_admin_flow="$(
  curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/flow/default-authentication-flow/
)"
if printf '%s' "$auth_admin_flow" | grep -aq '<style data-id="nodedc-auth-login-css">'; then
  echo 'admin host still has NODE.DC auth CSS'
  exit 1
fi
echo 'auth-admin-css-ok'
```

## Backup текущего состояния

С Mac, при смонтированном `/Volumes/docker`:

```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
NAS_ROOT=/Volumes/docker/nodedc-platform ./infra/synology/backup-current.sh
```

Файловый backup создаётся в `/Volumes/docker/nodedc-platform/backups/platform-current-*`.

Для Authentik Postgres dump нужно выполнить напечатанную команду на Synology, потому что Docker доступен через интерактивный `sudo`:

```bash
bash /volume1/docker/nodedc-platform/backups/platform-current-YYYYMMDD-HHMMSS/run-authentik-db-dump-on-synology.sh
```

Для Notification Core Postgres dump:

```bash
bash /volume1/docker/nodedc-platform/backups/platform-current-YYYYMMDD-HHMMSS/run-notification-db-dump-on-synology.sh
```

Если планируются изменения Tasker backend/schema, дополнительно выполнить:

```bash
bash /volume1/docker/nodedc-platform/backups/platform-current-YYYYMMDD-HHMMSS/run-tasker-db-dump-on-synology.sh
```

Если Ops Agents Gateway уже был запущен и там есть production tokens/grants/audit, дополнительно выполнить:

```bash
bash /volume1/docker/nodedc-platform/backups/platform-current-YYYYMMDD-HHMMSS/run-ops-agents-db-dump-on-synology.sh
```

## Что нужно перед запуском

- Собрать или загрузить `linux/amd64` images:
  - `nodedc/launcher:local`
  - `nodedc/notification-core:local`
  - `nodedc/plane-frontend:ru`
  - `nodedc/plane-admin:ru`
  - `nodedc/plane-space:ru`
  - `nodedc/plane-live:local`
  - `nodedc/plane-backend:local`
  - `nodedc/plane-proxy:ru`
- Для Ops Agents Gateway отдельный registry image пока не обязателен: deploy из source repo выполняется через `docker compose --env-file .env -f docker-compose.synology.yml up -d --build`.
- Создать `.env.synology` из `.env.synology.example` и заменить все `replace-with-*`.
- Создать `plane.env.synology` для Tasker из `plane.env.staging.example`, но с HTTP URL на `*.nas.nodedc:18080` и портами `18090/18490`.

## Обязательные runtime-права

Launcher пишет runtime snapshot и uploads под пользователем `node` (`uid=1000`). После создания NAS-папок:

```bash
cd /volume1/docker/nodedc-platform/platform
sudo mkdir -p ../launcher/server-storage ../launcher/uploads
sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads
sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads
```

Проверка внутри контейнера:

```bash
sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \
  'touch /app/server/storage/.write-test /app/server/storage/uploads/.write-test && rm /app/server/storage/.write-test /app/server/storage/uploads/.write-test && echo storage-ok'
```

## Проверки после деплоя

```bash
curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \
  | grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ'
```

В выводе должны быть `id.nodedc.ru -> hub.nodedc.ru` и не должно быть `launcher.local`.
