NODEDC_DESIGN_GUIDELINE/docs/FOUNDRY_MAP_CESIUM_CANON.md

52 KiB
Raw Blame History

Foundry Map Page + Cesium: production canon

Статус: действующий production-контракт

Область: Module Foundry Map Page, Platform Map Gateway, общий TileCache, DC AMD Proxy и DC AMD Connector

Контрольная дата: 2026-07-16

Этот документ — воспроизводимый канон интеграции Cesium в NODE.DC. Он описывает не только текущий Foundry Map Page, но и обязательные границы для следующих продуктов NODE.DC, которым потребуются Cesium Terrain, imagery, 3D Tiles или другие разрешённые provider assets.

Текущий проверенный deploy baseline:

  • dc-amd-proxy-transport-lifecycle-20260716-005;
  • platform-map-gateway-hot-path-20260716-015;
  • module-foundry-map-hot-path-20260716-012.

Идентификаторы baseline нужны для аудита. Их нельзя повторно использовать для новых изменений: каждый следующий deploy получает новый patch id и новый digest.

1. Главные инварианты

  1. Browser никогда не получает Cesium Ion master token, asset-scoped token, Bing key, Map Gateway admin secret или egress/connector secret.
  2. Foundry не хранит Cesium token в .env, runtime layout, Application Manifest, browser storage, deploy artifact или исходном коде.
  3. Единственный владелец Cesium master token и provider endpoint credentials — private Platform Map Gateway.
  4. Все browser-запросы карты идут same-origin через Foundry BFF. Browser не обращается напрямую ни к Map Gateway, ни к AMD Proxy, ни к Cesium/Bing.
  5. Один Platform Map Gateway и один NAS-resident mutable TileCache обслуживают все Foundry Applications, страницы, пользователей и browser-инстансы.
  6. Warm cache hit читается прямо с NAS и не требует Ion token refresh, AMD Proxy, Windows-машины, VPN или доступности Cesium.
  7. Только cold miss или явный refresh проходит по внешнему маршруту Map Gateway → DC AMD Proxy → DC AMD Connector → VPN → Cesium/Bing.
  8. NAS networking, DNS, default route, Tailscale и VPN не изменяются. Через AMD-машину идёт только allowlisted Cesium/Bing traffic.
  9. TileCache не лежит в Git, Docker image layer или application volume. Это отдельный persistent NAS bind directory.
  10. Записанный объект не удаляется автоматически и не перезаписывается без явного nodedc_cache_refresh=1.
  11. Partial response никогда не становится cache entry. Object публикуется atomic rename, а fill считается завершённым только после durable snapshot индекса.
  12. Все deploy-изменения доставляются data-only artifact через canonical runner: сначала plan точного файла, затем apply этого же файла. Live edit и ручной docker compose up на NAS не являются deploy-каноном.

2. Источники истины в репозитории

Канон должен обновляться вместе с этими файлами:

  • Foundry BFF и admin settings: server/catalog-server.mjs;
  • Foundry session/auth boundary: server/nodedc-auth.mjs;
  • UI TileCache и health state: apps/catalog/src/MapFixturePreview.tsx;
  • Cesium adapter и provider startup: apps/catalog/src/CesiumMapRenderer.tsx;
  • Platform provider/cache boundary: ../../platform/services/map-gateway/src/server.mjs;
  • Map Gateway runtime topology: ../../platform/infra/synology/docker-compose.platform-http.yml;
  • NAS egress service: ../../platform/services/dc-amd-proxy/server.mjs;
  • AMD egress installation, recovery and migration runbook: ../../platform/services/dc-amd-proxy/OPERATIONS.md;
  • Windows connector: ../../platform/services/dc-amd-connector/;
  • одноразовое pairing: ../../platform/tools/dc-amd-pair/;
  • canonical artifact builders: ../../platform/infra/deploy-runner/.

Если текст этого документа расходится с уже задеплоенным кодом, сначала фиксируется расхождение в Ops, затем синхронно исправляются код, тесты и этот канон. Нельзя молча считать устаревший текст runtime-поведением.

3. Архитектура и владельцы

Authenticated browser
  │ same-origin GET/HEAD + session cookie
  ▼
Foundry BFF
  │ trusted subject header, no provider credential
  ▼
Platform Map Gateway
  ├─ warm hit ───────────────► NAS live TileCache ─► browser
  │
  └─ miss / explicit refresh
       │ private egress token
       ▼
     DC AMD Proxy on NAS 172.22.0.222:8790
       │ authenticated CONNECT transport
       ▼
     DC AMD Connector on Windows 172.22.0.183:8791
       │ workstation VPN exit
       ▼
     Cesium Ion / Cesium assets / Bing imagery
       │ streamed response
       ├─► browser immediately
       └─► private temp file ─atomic rename─► NAS TileCache

3.1 Responsibility table

Компонент Владеет Не владеет
Map Page provider-neutral scene, presentation, camera, cache intent tokens, upstream host policy, cache files, VPN
Cesium adapter преобразование scene в Cesium objects, запрос allowlisted asset endpoints master token, direct provider networking
Foundry BFF user session, same-origin proxy, admin authorization, HMAC signing provider credentials, TileCache, VPN route
Map Gateway provider allowlist, credential injection, endpoint metadata, TileCache policy/index, upstream diagnostics UI layout, Windows VPN configuration
NAS live TileCache общие immutable-by-default provider objects и индекс Git source, per-user state, offline license
DC AMD Proxy узкий authenticated egress, pooling, retry/redirect safety, transport metrics general proxying, NAS route, provider token persistence
DC AMD Connector CONNECT byte forwarding через текущий Windows VPN provider URL parsing above host:443, master token, NAS routing
Windows workstation стабильный LAN endpoint, Docker Desktop, VPN exit Foundry/Map Gateway runtime state
deploy runner root-owned secrets, runtime directories, atomic artifact apply/rollback application UI, token entry

3.2 Один writer для общего cache

Текущая topology предполагает один активный map-gateway process, который является единственным writer для index.json. Все Foundry-инстансы используют его через private Docker network.

Масштабирование Foundry горизонтально безопасно: cache общий. Горизонтальное масштабирование самого Map Gateway без доработки запрещено, потому что tile singleflight и coalescing индекса сейчас process-local. Перед запуском нескольких Gateway replicas нужен распределённый per-key lock и согласованный single-writer/transactional index.

4. Два независимых маршрута

4.1 Control plane: настройка Cesium Ion

Foundry admin browser
  → PUT /api/platform-settings/cesium-ion
  → Foundry revalidates admin session
  → HMAC-signed PUT /api/map/admin/cesium-ion
  → Map Gateway verifies assets 1, 2, 96188
  → atomic private token write

Порядок принципиален:

  1. UI принимает новое значение как password field по существующей HTTPS-сессии.
  2. Foundry повторно проверяет server-side роль; скрытая кнопка сама по себе не является защитой.
  3. Foundry не сохраняет token, а формирует HMAC v2 по method, pathname, timestamp, actor id, SHA-256 body и нормализованному Foundry referer.
  4. Map Gateway принимает admin request только с валидной подписью и timestamp в пределах 60 секунд.
  5. Candidate token проверяется параллельно по canonical assets:
    • 1 — Cesium World Terrain;
    • 2 — World Imagery/Bing endpoint;
    • 96188 — Cesium OSM Buildings.
  6. Только если все три проверки успешны, рабочее значение атомарно заменяется.
  7. Ошибочный candidate не уничтожает предыдущий рабочий token.
  8. Browser получает только configured, verification, updatedAt, updatedBy; значение token не возвращается.

Успешная rotation увеличивает внутреннее поколение token, очищает in-memory и disk endpoint credentials для allowlisted assets и не трогает уже записанные tile objects. In-flight refresh старого поколения не может вернуть старый credential после rotation.

4.2 Data plane: рендер карты

При старте Map Page Cesium adapter независимо и параллельно запрашивает imagery, terrain и buildings. Отказ imagery не блокирует terrain; отказ buildings не останавливает остальную сцену.

GET /api/map/runtime-config
GET /api/map-gateway/api/map/ion/assets/2/endpoint
GET /api/map-gateway/api/map/ion/assets/1/endpoint
GET /api/map-gateway/api/map/ion/assets/96188/endpoint

Endpoint response содержит публичный URL, attribution, тип и credentialMode: "gateway". Затем Cesium DefaultProxy направляет все derived resources через:

/api/map-gateway/api/map/cache?url=<encoded-provider-url>

Master token или asset credential в этом URL отсутствует. Map Gateway добавляет credential server-side только после cache lookup и только если действительно нужен upstream request.

5. Credential model

5.1 Cesium Ion master token

Canonical location внутри mutable Map Gateway volume:

/var/lib/nodedc-map-live-cache/secrets/cesium-ion-token

Физически на NAS:

/volume1/docker/nodedc-platform/map-gateway/live-tile-cache/secrets/cesium-ion-token

Directory создаётся с mode 0700, token и metadata — 0600. Private NAS file имеет приоритет над transitional CESIUM_ION_TOKEN bootstrap после restart.

Нельзя:

  • помещать token в Foundry .env;
  • собирать его во frontend bundle;
  • вставлять в Application Manifest;
  • отправлять в chat, Ops comment, diagnostic archive или deploy artifact;
  • читать его обратно через UI;
  • включать live-tile-cache/secrets в обычный tile export или SMB-операции.

5.2 Map Gateway admin secret

Runner-owned path:

/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret

Он монтируется read-only в Foundry и Map Gateway как /run/nodedc-secrets/map-gateway-admin-secret. Это ключ межсервисной подписи, а не Cesium token и не продуктовая настройка. Он никогда не вводится пользователем.

5.3 Map egress proxy token

Runner-managed path:

/volume1/docker/nodedc-platform/secrets/map-egress-proxy-token

Map Gateway читает его из read-only mount и ставит в x-proxy-token только при запросе к DC AMD Proxy. Browser и Windows connector его не получают.

5.4 AMD connector access token

Windows installer создаёт локальный random value в:

C:\NODEDC\dc-amd-connector\runtime\connector-access

Одноразовый pairing script читает его внутри запущенного container и передаёт напрямую на NAS без печати. NAS сохраняет paired value в private runtime dc-amd-proxy; deploy artifact его не содержит.

5.5 Asset-scoped endpoint credentials

Ответ Ion endpoint API может содержать временный accessToken либо Bing key. Map Gateway хранит эти значения только в памяти и private files:

live-tile-cache/ion-endpoints/<assetId>.json

Файлы имеют mode 0600 и считаются service-sensitive. Master token в них не записывается.

Перед выдачей endpoint browser-у Gateway удаляет credential query parameters. Перед upstream miss Gateway:

  1. снова удаляет user-supplied token, key, signature и аналогичные параметры;
  2. сопоставляет resource с endpoint по точному origin и наиболее длинному разрешённому path scope;
  3. для file endpoint, например tileset.json, разрешает sibling resources только внутри его directory;
  4. никогда не превращает root-level file endpoint в credential scope всего host;
  5. добавляет соответствующий asset credential или Bing key.

Endpoint cache имеет TTL 300 секунд и refresh-ahead 60 секунд по умолчанию. Refresh одного asset singleflight-ится. Истёкший JWT не выдаётся, не сохраняется и не подставляется.

5.6 Referer restrictions

Если Cesium token ограничен URL/referer policy, production FOUNDRY_PUBLIC_URL должен совпадать с разрешённым Foundry origin. Foundry делегирует только собственный configured origin, а не произвольный browser header. Нормализованный referer входит в HMAC admin request и передаётся в provider requests.

Прямая terminal-проверка token без того же referer может вернуть 401, хотя production flow работает. Каноническая проверка — через Foundry settings/Map Gateway, потому что она воспроизводит реальный транспорт и проверяет все три обязательных asset.

6. Физическое устройство TileCache

6.1 NAS paths

Mutable live cache:

/volume1/docker/nodedc-platform/map-gateway/live-tile-cache

Container mount:

/var/lib/nodedc-map-live-cache

Read-only offline snapshot:

/volume1/docker/nodedc-platform/map-gateway/offline-snapshot

Container mount:

/var/lib/nodedc-map-offline-snapshot

Обе host directory создаёт root-owned deploy runner. docker compose down, image rebuild и service restart их не удаляют.

6.2 On-disk format

live-tile-cache/
├── index.json
├── objects/
│   └── <first-two-hash-chars>/
│       └── <sha256-cache-key>.bin
├── ion-endpoints/
│   ├── 1.json
│   ├── 2.json
│   └── 96188.json
└── secrets/
    ├── cesium-ion-token
    └── cesium-ion-token.metadata.json

objects и index.json — собственно TileCache. ion-endpoints и secrets — private service state; они не являются пользовательскими cache objects.

index.json version 1 содержит для каждого SHA-256 key:

  • relative file path;
  • byte count;
  • content type;
  • provider ETag, если он корректен;
  • savedAt, lastAccessAt, expiresAt.

Binary object сохраняется с расширением .bin независимо от media type; content type берётся из индекса. Cache key — SHA-256 canonical provider URL.

6.3 Canonical URL and deduplication

До вычисления key удаляются:

  • nodedc_client_revision;
  • nodedc_cache_profile;
  • nodedc_cache_mode;
  • nodedc_cache_refresh;
  • credential-like query parameters.

Query parameters сортируются. Bing subdomains ecn.t0ecn.t3 нормализуются в один logical host, чтобы одинаковый quadkey не создавал четыре копии.

Следствие: token rotation, новый browser build или другой Foundry Application не создают новый tile object для тех же provider bytes.

7. Product semantics of cache controls

7.1 Кэшировать live-данные выключено

Adapter добавляет nodedc_cache_mode=passthrough. Запрос всё равно проходит через Foundry и Map Gateway, поэтому credential/security boundary сохраняется, но live persistent cache не читается и не записывается.

Это не direct browser-to-Cesium режим.

7.2 Cache включён + Не перезаписывать уже полученный cache включено

Это основной steady-state режим:

  • hit немедленно отдаётся с NAS;
  • miss идёт к official upstream и после полного получения дописывается;
  • существующий объект не заменяется даже после его provider TTL;
  • отключение AMD/VPN не влияет на уже записанную область.

Галка не замораживает cache целиком. Она запрещает перезапись существующих key, но новые tiles, которые пользователь впервые открыл, продолжают долетать и записываться на NAS.

7.3 Cache включён + Не перезаписывать выключено

Adapter добавляет nodedc_cache_refresh=1 в provider resource root. Derived requests текущего Application могут заменить существующие objects. Это явно разрешённый update mode, а не автоматический фоновый refresh всего cache.

Обновляются только ресурсы, которые реально запросил renderer; мировой dataset не обходится целиком.

7.4 Обновить текущий viewport

Это одноразовый refresh. Renderer пересоздаётся с nodedc_cache_refresh=1, помечает root provider resources текущего view, после первого render возвращается к сохранённой steady-state policy.

7.5 Gateway-level modes

MAP_CACHE_MODE Hit Miss Запись
readwrite NAS official upstream да, после полного response
readonly NAS live pass-through нет
offline NAS/offline policy 504 нет и никакого egress

Application UI не может изменить server-level MAP_CACHE_MODE.

7.6 Live vs offline profile

nodedc_cache_profile=live выбирает mutable store. offline выбирает read-only snapshot и никогда не допускает pass-through.

Offline mode дополнительно требует hostname в MAP_GATEWAY_OFFLINE_PROVIDER_ALLOWLIST. Пустой список по умолчанию означает осознанный запрет offline redistribution. Наличие технически сохранённых Cesium bytes само по себе не создаёт право на их offline distribution.

8. Request algorithm

Для каждого /api/map/cache Gateway выполняет следующий порядок:

  1. Проверяет trusted subject, method и route.
  2. Парсит и валидирует HTTPS target, host allowlist, URL length и отсутствие embedded credentials.
  3. Нормализует cache profile/mode/refresh intent и удаляет их из provider URL.
  4. Вычисляет credential-free canonical key.
  5. Для passthrough пропускает persistent store и идёт к upstream через тот же security boundary.
  6. Для normal mode сначала ищет object в выбранном store.
  7. Если по этому key уже идёт fill/refresh, новый request становится follower и ждёт его commit.
  8. Offline/legacy profile никогда не превращает miss в неожиданный upstream fetch.
  9. Normal hit без explicit refresh сразу отдаётся с NAS, в том числе stale hit.
  10. Только после отсутствия пригодного hit Gateway получает/обновляет endpoint credential и подставляет его в upstream URL.
  11. readonly отдаёт live response без записи.
  12. Range request проксируется как range и пока не записывается partial object.
  13. Full miss/refresh запускает streaming fill.
  14. При provider failure explicit refresh возвращает предыдущий cached object как live-stale-upstream-error, если он существует.
  15. При заполненном cache cold miss показывается live как live-pass-through-cache-full; существующие objects не удаляются.

Этот порядок гарантирует ключевой fail-safe: warm cache не зависит от control plane и egress.

9. Concurrency and performance

9.1 Что происходит при 20 одновременных пользователях

Для одного и того же missing key:

  1. Первый request становится leader и открывает один upstream stream.
  2. Response body делится через stream tee.
  3. Client branch начинает передаваться первому browser сразу после provider headers.
  4. Cache branch независимо пишется во временный файл.
  5. Остальные request того же key становятся followers и не создают новые provider downloads.
  6. После atomic commit followers читают опубликованный NAS file.
  7. Если один browser закрыл вкладку, server-owned cache fill не отменяется и продолжает обслуживать других.

Для разных keys requests выполняются параллельно. Capacity reservations сериализуют только расчёт общего объёма, а не весь network path.

9.2 Publication safety

Cache fill:

  • пишет только в private *.tmp;
  • считает фактические bytes и прерывает object сверх лимита;
  • резервирует capacity с учётом replacement;
  • делает atomic rename только после полного EOF;
  • затем добавляет entry в memory index и ждёт durable index snapshot;
  • при abort/error удаляет temp и не публикует index entry.

9.3 Index strategy

Warm hit не обновляет lastAccessAt и вообще не переписывает index.json. Это осознанное следствие no-eviction policy: запись полного O(N) JSON на каждый 20 KB tile делала warm views медленнее provider.

Завершения разных new objects объединяются в короткое 10 ms окно. Один atomic index snapshot включает все накопленные revisions; waiter каждого object завершается только после durable revision.

9.4 ETag and browser cache

NAS hit получает безопасный ETag: валидный provider ETag либо deterministic NODE.DC ETag. If-None-Match возвращает 304 без чтения body и без записи index.

Foundry BFF разрешает browser cache только для подтверждённых persistent hit/stale states:

Cache-Control: private, max-age=300, stale-while-revalidate=60
Vary: Cookie

Provider pass-through, endpoint metadata, health, explicit refresh и errors получают no-store. Shared reverse proxy не должен раздавать authenticated map response между пользователями.

9.5 AMD connection pool

DC AMD Proxy держит отдельный HTTP/1.1 keep-alive pool на каждый approved origin:

  • максимум 8 active sockets per origin;
  • максимум 4 idle sockets per origin;
  • LIFO reuse;
  • keep-alive 30 секунд;
  • excess requests ждут в agent queue за bounded socket pool; её фактический пик обязательно контролируется через maxQueuedRequests.

Это устраняет новый NAS → AMD CONNECT → TLS handshake для каждого tile. При насыщении throughput ограничивается VPN/provider, а не созданием неограниченного числа tunnel. maxQueuedRequests, queue/connection/TTFB/total timings показывают, где возникает задержка.

Только idempotent GET/HEAD, потерявший уже reused keep-alive socket, повторяется один раз. Fresh-socket failure не ретраится автоматически. При cross-origin redirect Authorization удаляется; master/API bearer не может уйти с api.cesium.com на assets/Bing origin.

10. Timeouts, aborts and recovery

10.1 Foundry BFF

  • headers deadline: 15 секунд по умолчанию;
  • body idle deadline: 30 секунд без прогресса;
  • body timeout сбрасывается каждым chunk;
  • после upstream EOF медленный browser drain не ограничивается абсолютным таймером;
  • browser abort отменяет BFF upstream request;
  • ошибка после отправки headers закрывает stream, а не пытается отправить второй JSON response.

10.2 Map Gateway

  • upstream timeout: 30 секунд по умолчанию;
  • warm hit не запускает этот timeout;
  • refresh failure с прежним object даёт stale fallback;
  • client abort leader response не отменяет cache branch;
  • headers-sent error уничтожает только response, не Gateway process.

10.3 DC AMD Proxy

  • connector/TLS connection timeout: 20 секунд;
  • provider headers/attempt timeout: 30 секунд;
  • response body idle timeout: 30 секунд без bytes;
  • abort проходит через queue, CONNECT, TLS, headers, redirect drain, retry и body;
  • каждый logical request получает ровно один terminal accounting outcome;
  • partial bytes учитываются отдельно от complete response bytes.

10.4 Cesium renderer

Provider startup независим. Render error не показывает стандартную Cesium modal с [object Object]; UI получает safe code и делает одну bounded попытку восстановить render loop. Постоянный GPU/browser fault не запускает бесконечный retry.

11. Authentication and authorization

Production Foundry всегда использует Launcher/Authentik session. Map tile burst не должен валидировать Launcher на каждый object, поэтому:

  • validation TTL по умолчанию 20 секунд и зажат в production диапазоне 1530 секунд;
  • запросы одной session делят singleflight validation;
  • transient Launcher error использует 2-секундный retry backoff;
  • last-known-good identity допускается не более 30 секунд grace и только для GET/HEAD;
  • mutation во время transient auth failure получает 503;
  • только явный {ok:true, active:false} удаляет session/cookie;
  • timeout, network error и Launcher non-2xx не уничтожают рабочую cookie;
  • session хранится process-local, поэтому после Foundry restart старая opaque cookie истекает и нужен новый handoff.

Foundry roles:

  • nodedc:module-foundry:admin — Platform settings;
  • nodedc:module-foundry:user — обычная работа без settings;
  • nodedc:module-foundry:blocked — deny-first;
  • nodedc:module-foundry:access — transitional user role;
  • nodedc:superadmin и user_root — break-glass admin.

Raw groups не возвращаются browser-у. Map Gateway production route требует trusted x-nodedc-user-id, который создаёт только Foundry BFF/private health check. Gateway нельзя публиковать напрямую в Internet.

12. DC AMD route

12.1 NAS service

dc-amd-proxy работает с network_mode: host, но слушает только 172.22.0.222:8790. Host networking нужен, чтобы one-time pairing видел реальный source IP Windows host; service не bind-ится на все NAS interfaces.

Он принимает:

  • GET /healthz и GET /status — safe state/metrics;
  • POST /api/pair — один раз и только с configured AMD LAN source;
  • GET|HEAD /proxy/cesium/fetch — только с runner-synchronised map egress token.

Target обязан быть HTTPS port 443 и входить в фиксированный Cesium/Bing allowlist. Direct NAS egress fallback отсутствует (directEgress: false).

12.2 Windows connector

dc-amd-connector bind-ится к стабильному LAN IP 172.22.0.183:8791, а не к OpenVPN address или public exit IP. Windows Firewall разрешает inbound только с NAS 172.22.0.222.

Connector:

  • принимает только authenticated HTTP CONNECT;
  • разрешает только port 443 и тот же host allowlist;
  • не расшифровывает end-to-end TLS и не видит Ion bearer;
  • не является системным Windows proxy;
  • не меняет routing/DNS/VPN других приложений;
  • ограничен 1 CPU, 256 MB, 64 processes и 4096 file descriptors.

Docker Compose использует restart: unless-stopped. Docker Desktop на WSL2 восстанавливается через Startup launcher после входа configured Windows user. До sign-in это не unattended Windows service; в это окно warm TileCache продолжает работать, а cold misses ожидаемо недоступны.

12.3 Pairing and moving to another machine

Первичная установка выполняется из elevated PowerShell внутри проверенного connector package:

powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1

После того как NAS proxy показывает state=awaiting_pair, на Windows запускается package из platform/tools/dc-amd-pair:

powershell -NoProfile -ExecutionPolicy Bypass -File .\pair-dc-amd-proxy.ps1

Ожидаемый результат: AMD connector paired with NAS. No secret value was displayed.

Перенос:

  1. Выбрать новый стабильный LAN IP и проверить VPN на новой машине.
  2. Установить тот же versioned connector package с явными -BindAddress и -NasAddress.
  3. Проверить local container health и permitted CONNECT.
  4. Подготовить новый NAS proxy artifact/config с новым connector host и pair source.
  5. Реализовать и проверить runner-owned pair-rotation transition: текущий NAS pair write-once и не принимает fresh credential поверх существующего state.
  6. Через этот transition вывести старую pairing identity, выполнить canonical plan, apply, one-time pairing и end-to-end validation.
  7. Остановить старый connector только после успешного cold-request acceptance на новой машине.

Текущий runner ещё не предоставляет pair rotation как готовую операцию. До её появления миграция с fresh credential не является полностью канонической: нельзя вручную удалять NAS runtime/connector-access, копировать старый secret, ослаблять /api/pair или одновременно держать две pairing identity. Сначала создаётся отдельная Ops/change задача на audited rotation, затем выполняется перенос по platform/services/dc-amd-proxy/OPERATIONS.md.

13. Health and observability

13.1 Safe checks on NAS

curl -fsS http://172.22.0.222:8790/status

curl -fsS \
  -H 'x-nodedc-user-id: healthcheck' \
  http://127.0.0.1:18103/healthz

curl -fsS http://172.22.0.222:9920/healthz

Эти команды не печатают secrets. Для endpoint acceptance нужно выводить только safe fields, а не private files:

curl -fsS \
  -H 'x-nodedc-user-id: healthcheck' \
  http://127.0.0.1:18103/api/map/ion/assets/1/endpoint \
  | /usr/local/bin/docker exec -i nodedc-platform-map-gateway-1 \
      node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const v=JSON.parse(s);console.log(JSON.stringify({ok:v.ok,assetId:v.assetId,type:v.type,credentialMode:v.credentialMode,cache:v.cache}))})"

Если конкретное имя container изменилось, использовать docker compose ps, а не угадывать новое имя.

13.2 Map Gateway health fields

/healthz возвращает:

  • live/offline cache entry count and bytes;
  • mode, writePolicy, maxBytes, atCapacity, persistent;
  • hit/stale/miss/refresh/passthrough/fallback counters;
  • upstream and egress request/failure counters;
  • slow upstream count;
  • index snapshot count;
  • safe lastFailure и timestamp;
  • ionConfigured и asset allowlist.

Он не возвращает token, provider URL с credential или endpoint credential.

13.3 AMD status fields

/status обязан показывать:

  • state=paired;
  • forwarding=amd_connector_only;
  • directEgress=false;
  • active/idle/queued sockets и pool limits per origin;
  • logical, terminal, in-flight, complete, failed and aborted requests;
  • opened/failed tunnels, socket reuse, bounded retries;
  • complete/partial bytes;
  • queue, connection, TTFB, retry, attempt and total timings;
  • safe last error code.

Инвариант accounting:

terminalRequests + inFlightRequests == requests

После завершившейся нагрузки inFlightRequests должен вернуться в 0.

13.4 UI health state

Когда Inspector или Layers открыт, Foundry:

  • проверяет runtime config и Gateway health каждые 15 секунд;
  • держит только один concurrent health request;
  • отменяет check через 10 секунд;
  • различает runtime HTTP, health HTTP, invalid response, network и timeout;
  • сохраняет last-known-good stats как stale, если новая проверка не прошла;
  • не даёт запоздалому renderer health overwrite более свежий explicit poll.

Красное сообщение gateway_not_configured относится к отсутствию runtime profile, а не автоматически к AMD/VPN. Код ошибки должен сохраняться до UI, иначе оператор ищет проблему не в том слое.

14. Failure matrix

Симптом / safe code Слой Что продолжает работать Проверка / действие
module_foundry_auth_required Foundry session public health пройти Launcher handoff; не обходить BFF
module_foundry_auth_unavailable Launcher validation bounded read-only grace, если ещё активен Launcher health/internal token; mutation не повторять вслепую
map_gateway_not_configured Foundry runtime config Foundry shell проверить NODEDC_MAP_GATEWAY_INTERNAL_URL и private Docker network
map_gateway_headers_timeout BFF→Gateway shell и уже browser-cached objects Gateway health, CPU/I/O, request queue
map_gateway_body_idle_timeout BFF stream прочие resources Gateway/AMD body progress, VPN stalls
map_gateway_auth_required Gateway trusted subject admin signed route/health с корректным header BFF header boundary, не включать anonymous в production
cesium_ion_not_configured Gateway credential store existing object hits admin settings; не добавлять token в Foundry env
cesium_ion_token_verification_failed provider/control plane предыдущий token и cache assets 1/2/96188 permissions, Foundry referer, AMD route
cesium_asset_not_allowed asset policy canonical assets изменить root-owned allowlist только с reviewed use case
cesium_ion_endpoint_unavailable Ion API warm object hits, usable cached endpoint AMD state/timings, VPN, provider HTTP status, token/referer policy
map_egress_amd_connector_not_paired NAS proxy pairing warm hits /status, затем controlled pairing
map_egress_amd_connector_timeout NAS→Windows warm hits/stale refresh fallback Windows host online, LAN/firewall, connector container
map_egress_amd_upstream_tls_timeout Windows/VPN→provider warm hits/stale refresh fallback VPN exit, DNS/provider reachability on Windows
map_upstream_timeout Gateway upstream warm hits/stale refresh fallback distinguish queue/CONNECT/TLS/TTFB via AMD metrics
map_cache_capacity_reached NAS capacity policy old hits and live pass-through provision capacity/export; do not delete arbitrary objects
map_offline_snapshot_miss offline snapshot other snapshot objects expected miss; seed licensed dataset, never fall through live
map_provider_offline_not_permitted provider policy live mode if allowed legal/provider review and explicit allowlist
map_cache_object_too_large object guard other objects/live policy inspect asset type; change limit only after measured review
live-stale-upstream-error explicit refresh failed previous object expected resilience; investigate upstream asynchronously
renderer cesium_render_error browser/GPU/Cesium shell and provider diagnostics browser console/GPU; one recovery already attempted

14.1 Reading latency correctly

  • Высокий queueMs, нормальный connection/TTFB: pool saturated; проверить request fan-out и VPN throughput.
  • Высокий connectionMs: дорогой NAS→AMD CONNECT или TLS setup; socket reuse недостаточен либо Windows/VPN нестабилен.
  • Низкий connection, высокий ttfbMs: VPN/provider latency.
  • Нормальный AMD timing, высокий Foundry body idle: проблема stream progress между Gateway и BFF либо NAS I/O.
  • Много cacheHits, но медленная карта: проверить BFF/browser ETag, NAS read latency, renderer/GPU; AMD здесь не должен участвовать.
  • egressRequests растёт на повторном одинаковом append-only viewport: cache key/refresh policy нарушена либо requests действительно относятся к новым tiles/LOD.

15. Canonical deploy and rollback

15.1 Pre-deploy validation

Foundry:

cd NODEDC_DESIGN_GUIDELINE
node --test server/*.test.mjs
npm run build

Map Gateway:

cd platform/services/map-gateway
npm run test:credential-boundary
npm run test:admin-token-boundary
npm run test:live-cache-fallback
npm run test:streaming-cache-fill

DC AMD Proxy:

cd platform/services/dc-amd-proxy
npm run test:connection-pool
docker compose config >/dev/null

Production images должны дополнительно собираться с нуля до публикации artifact. Ни один test fixture token не переносится в artifact.

15.2 Build data-only artifacts

Из repository root с новыми уникальными patch ids:

node platform/infra/deploy-runner/build-dc-amd-proxy-artifact.mjs \
  dc-amd-proxy-<change>-YYYYMMDD-NNN

node platform/infra/deploy-runner/build-map-gateway-artifact.mjs \
  platform-map-gateway-<change>-YYYYMMDD-NNN

node platform/infra/deploy-runner/build-module-foundry-artifact.mjs \
  module-foundry-<change>-YYYYMMDD-NNN

Artifact обязан содержать только manifest.env, files.txt, payload/**; запрещены secrets, live .env, runtime data, cache objects, symlinks, hooks и AppleDouble ._*.

Создать SHA-256 sidecar, проверить tar listing и скопировать .tgz + .sha256 через SMB в:

/volume1/docker/nodedc-deploy/inbox

15.3 Plan before apply

На NAS:

sudo /usr/local/sbin/nodedc-deploy verify-install

sudo /usr/local/sbin/nodedc-deploy plan \
  /volume1/docker/nodedc-deploy/inbox/<exact-dc-amd-proxy-artifact>.tgz

sudo /usr/local/sbin/nodedc-deploy plan \
  /volume1/docker/nodedc-deploy/inbox/<exact-platform-artifact>.tgz

sudo /usr/local/sbin/nodedc-deploy plan \
  /volume1/docker/nodedc-deploy/inbox/<exact-module-foundry-artifact>.tgz

До apply оператор проверяет digest, component, type=app-overlay, payload/compose roots, services, runtime secret mounts, exact files и state=new. sha-already-applied, неожиданный file или component — стоп, а не повод обходить runner.

15.4 Apply order

1. dc-amd-proxy
2. platform / map-gateway
3. module-foundry

Команды используют те же exact paths, что прошли plan:

sudo /usr/local/sbin/nodedc-deploy apply \
  /volume1/docker/nodedc-deploy/inbox/<exact-dc-amd-proxy-artifact>.tgz

sudo /usr/local/sbin/nodedc-deploy apply \
  /volume1/docker/nodedc-deploy/inbox/<exact-platform-artifact>.tgz

sudo /usr/local/sbin/nodedc-deploy apply \
  /volume1/docker/nodedc-deploy/inbox/<exact-module-foundry-artifact>.tgz

Нельзя использовать apply latest, wildcard или вручную распаковывать payload в live roots. Runner создаёт backup и выполняет component health gate. Ошибка одного слоя должна остановить цепочку до следующего apply.

15.5 Post-deploy acceptance

  1. Все три containers running и healthy.
  2. AMD /status: paired, amd_connector_only, directEgress=false.
  3. Gateway /healthz: cache.persistent=true, ожидаемые paths/limits, ionConfigured=true.
  4. Canonical endpoints 1, 2, 96188 возвращают соответствующие types и credentialMode=gateway без credentials.
  5. Первый cold viewport начинает рисоваться до завершения disk commit.
  6. Повторный viewport увеличивает cache hits, но не egress requests для тех же objects.
  7. Выключение Windows VPN не ломает warm viewport; новый uncached viewport даёт диагностируемый transport error.
  8. Explicit refresh при недоступном provider отдаёт старый object, если он был.
  9. Foundry UI показывает last-known-good cache stats как stale, а не стирает их generic error.
  10. Никакой token/key не встречается в browser Network response, logs, artifact listing или Ops comment.

16. Reuse in future NODE.DC products

Новый продукт с Cesium не копирует token handling или TileCache внутрь себя. Он подключается к существующей платформенной capability.

Обязательный шаблон:

  1. Provider-neutral domain/page contract живёт в продукте.
  2. Renderer adapter получает только same-origin runtime config и sanitised asset endpoint.
  3. Product BFF подтверждает свою user/session policy и проксирует только разрешённые Map Gateway routes.
  4. BFF передаёт trusted subject из server-side identity, а не browser header.
  5. Все provider resources используют Gateway proxy; direct provider URL без proxy запрещён.
  6. Новый asset id добавляется в reviewed allowlist с ожидаемым type и acceptance test.
  7. Cache intent кодируется теми же profile/mode/refresh semantics.
  8. Cache физически остаётся общим Platform cache; продукт не создаёт per-instance copy.
  9. Admin token rotation остаётся одной Platform setting, а не повторяется в каждом продукте.
  10. Health UI различает runtime, Gateway, cache и egress layers и сохраняет safe error code.

Если продукту нужен другой provider или offline dataset, сначала расширяется Platform Map Gateway policy. Нельзя просто добавить arbitrary host в frontend. Требуются license review, host/path allowlist, credential scope, cache policy, diagnostics и tests.

17. Explicit decisions and forbidden anti-patterns

Решения

  • Shared NAS cache выбран вместо cache на browser/Foundry instance.
  • Append-only/no-eviction выбран для предсказуемости; capacity exhaustion не удаляет уже собранную карту.
  • Cache-first выполняется до credential injection, чтобы offline resilience была реальной.
  • First miss streaming выбран вместо «сначала полностью записать, потом показать».
  • One Map Gateway writer выбран до появления distributed locking.
  • AMD route ограничен provider hosts и не затрагивает NAS default networking.
  • Endpoint credentials кэшируются private и refresh-ятся заранее, master token остаётся отдельным.
  • Browser cache разрешён только для подтверждённых NAS hits и только private.

Запрещено

  • token в browser bundle, source, .env Foundry, Envoyer-like UI или Application artifact;
  • direct Cesium calls из browser;
  • proxy всего NAS/Windows traffic через VPN ради Cesium;
  • open/general-purpose HTTP proxy на AMD или NAS;
  • использование changing VPN IP вместо stable AMD LAN IP;
  • cache в Docker writable layer или anonymous volume;
  • отдельный TileCache на каждого пользователя/Application/Foundry replica;
  • credential query в cache key;
  • автоматический refresh каждого hit без product intent;
  • полный index.json rewrite на каждый warm hit;
  • новый CONNECT/TLS tunnel на каждый tile;
  • безусловный retry non-idempotent request или fresh-socket failure;
  • перенос Authorization через cross-origin redirect;
  • absolute body deadline, который убивает большой, но прогрессирующий response;
  • публикация partial file/index entry;
  • автоматический LRU delete без отдельной принятой retention policy;
  • silent direct NAS egress, если AMD/VPN недоступен;
  • трактовка сохранённых Cesium bytes как разрешения на offline distribution;
  • скрытие provider credits на внешнем/коммерческом surface. Текущее скрытие credit container допустимо только как зафиксированный sandbox debt;
  • ручной sudo/live edit вместо exact runner plan + apply.

18. Known boundaries and next extensions

  1. Range requests сейчас проходят live и не записываются как chunk cache. Range-aware cache добавляется только после измерения реальных 3D Tiles/Gaussian assets.
  2. Runtime cache сам не prefetch-ит мир. Для offline regions нужен отдельный licensed seed/prefetch job и versioned export/import artifact.
  3. Offline snapshot read-only; miss никогда не становится live request.
  4. index.json подходит текущему single-writer scale. Multiple Gateway writers требуют нового storage contract.
  5. Windows Docker Desktop recovery начинается после user sign-in. Если потребуется unattended SLA, connector должен стать отдельно спроектированным Windows service/host, а не скрытым изменением текущей схемы.
  6. DC AMD Proxy можно расширять на новые потоки только отдельным policy change. Нельзя добавлять arbitrary URL forwarding к существующему Cesium route.
  7. Provider attribution metadata сохраняется, но visible credit surface должен быть возвращён до внешнего release.

19. Definition of done for any Cesium change

Изменение считается завершённым только если:

  • credential boundary доказан automated test;
  • cache hit не делает provider/egress request;
  • same-key concurrency делает один upstream fill;
  • abort/partial response не публикует object;
  • refresh failure сохраняет старый object;
  • endpoint rotation не возвращает credential старого поколения;
  • BFF корректно передаёт conditional headers и abort;
  • session validation не умножается на tile count;
  • AMD metrics сохраняют exact terminal accounting;
  • logs не содержат URL query, token, cookie, session/user id или secret;
  • production images собираются с нуля;
  • data-only artifacts проходят inspection и checksum;
  • canonical runner показывает ожидаемый plan до apply;
  • post-deploy acceptance проверяет cold, warm, VPN-off и refresh-failure scenarios;
  • Ops card и этот документ обновлены вместе с кодом.

Итоговая модель проста: Platform Map Gateway владеет credential и общим NAS TileCache; Foundry владеет authenticated same-origin BFF и UI intent; AMD-машина предоставляет только узкий VPN egress. Warm data всегда возвращается локально, а live route используется только там, где cache действительно не хватает или оператор явно разрешил refresh.