848 lines
52 KiB
Markdown
848 lines
52 KiB
Markdown
# 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. Архитектура и владельцы
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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 не останавливает остальную сцену.
|
||
|
||
```text
|
||
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 через:
|
||
|
||
```text
|
||
/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:
|
||
|
||
```text
|
||
/var/lib/nodedc-map-live-cache/secrets/cesium-ion-token
|
||
```
|
||
|
||
Физически на NAS:
|
||
|
||
```text
|
||
/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:
|
||
|
||
```text
|
||
/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:
|
||
|
||
```text
|
||
/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 в:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
/volume1/docker/nodedc-platform/map-gateway/live-tile-cache
|
||
```
|
||
|
||
Container mount:
|
||
|
||
```text
|
||
/var/lib/nodedc-map-live-cache
|
||
```
|
||
|
||
Read-only offline snapshot:
|
||
|
||
```text
|
||
/volume1/docker/nodedc-platform/map-gateway/offline-snapshot
|
||
```
|
||
|
||
Container mount:
|
||
|
||
```text
|
||
/var/lib/nodedc-map-offline-snapshot
|
||
```
|
||
|
||
Обе host directory создаёт root-owned deploy runner. `docker compose down`, image rebuild и service restart их не удаляют.
|
||
|
||
### 6.2 On-disk format
|
||
|
||
```text
|
||
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.t0`…`ecn.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:
|
||
|
||
```text
|
||
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 диапазоне 15–30 секунд;
|
||
- запросы одной 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
|
||
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1
|
||
```
|
||
|
||
После того как NAS proxy показывает `state=awaiting_pair`, на Windows запускается package из `platform/tools/dc-amd-pair`:
|
||
|
||
```powershell
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```bash
|
||
cd NODEDC_DESIGN_GUIDELINE
|
||
node --test server/*.test.mjs
|
||
npm run build
|
||
```
|
||
|
||
Map Gateway:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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 в:
|
||
|
||
```text
|
||
/volume1/docker/nodedc-deploy/inbox
|
||
```
|
||
|
||
### 15.3 Plan before apply
|
||
|
||
На NAS:
|
||
|
||
```bash
|
||
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
|
||
|
||
```text
|
||
1. dc-amd-proxy
|
||
2. platform / map-gateway
|
||
3. module-foundry
|
||
```
|
||
|
||
Команды используют те же exact paths, что прошли `plan`:
|
||
|
||
```bash
|
||
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.**
|