e.active)
const isSel = selWanIdx === wIdx
return (
- {if(e.key==="Enter"||e.key===" "){e.preventDefault();setSelWanIdx(prev=>prev===wIdx?null:wIdx)}}}
onClick={() => setSelWanIdx(prev => prev === wIdx ? null : wIdx)}
className={cn(
"rounded-lg border p-3 cursor-pointer transition-all",
diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md
index cf0e377d..782ace5d 100644
--- a/docs/ux-ui-plan.md
+++ b/docs/ux-ui-plan.md
@@ -189,4 +189,4 @@ HTTP-фикстуры; локальная приёмка: 43 основных +
| Оболочка · [Frame](https://reui.io/components/frame?ref=mcp) / [API](https://reui.io/docs/components/base/frame?ref=mcp) | [Обзор](ux-ui/screens/network-map-overview-desktop.png) | [390 px](ux-ui/screens/network-map-overview-mobile.png), [320 px](ux-ui/screens/network-map-overview-narrow.png) | Источник → свежесть → слои/фильтры → карта |
| Детали · Frame / [List 9](https://reui.io/preview/base/list-9?ref=mcp) | [Детали](ux-ui/screens/network-map-detail-desktop.png) | [390 px](ux-ui/screens/network-map-detail-mobile.png), [320 px](ux-ui/screens/network-map-detail-narrow.png) | Устройство → фактические поля → действия → возврат |
-Оболочка и клавиатурные действия проверены (35 + 12 E2E). Масштаб малой топологии требует следующего блока; это промежуточные снимки, не итоговая приёмка карты.
+Оболочка и клавиатурные действия проверены (35 + 12 E2E). Камера и читаемость малой топологии доработаны в 10.53 (16 E2E + 2 unit). Следом pointer/touch и пустые состояния; итоговая приёмка карты ещё не завершена.
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md
index 4f355496..3048fee5 100644
--- a/docs/ux-ui-progress.md
+++ b/docs/ux-ui-progress.md
@@ -26,12 +26,20 @@
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
-| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.52; оптимизатор принят локально, карта сети в работе |
+| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.53; оптимизатор принят локально, карта сети в работе |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
+
+## Блок 10.53 — камера и читаемость карты (2026-10-01)
+
+- ViewBox учитывает реальное соотношение сторон canvas; начальное вписывание включает устройства, WAN, сервисы и подписи. Оно выполняется один раз на источник и не сбрасывает ручной zoom после обновления API. Кнопка/горячая клавиша вписывают текущие объекты; wheel/drag используют ту же геометрию.
+- Название вынесено под узел; на телефоне размер текста компенсирует SVG масштаб, длинная подпись сокращена при сохранённом полном accessible name/деталях. Страна обозначена ISO при отсутствии площадки и до загрузки внешнего изображения флага. Легенда — доступные раскрываемые обозначения вне SVG.
+- WAN карточка получила Enter/Space и aria-pressed. 2 camera unit + 16 E2E layout/details/URL, TypeScript и lint прошли; после ISO fallback повторены 5 layout E2E, TypeScript и lint. Снимки обновлены.
+- Далее pointer/touch, закрытие деталей с возвратом фокуса, пустой inventory/пустой срез; затем production build и локальная приёмка карты. Реальная сеть не проверена.
+
## Блок 10.52 — Frame, доступные действия и детали карты (2026-10-01)
- Установленный ReUI Frame объединяет toolbar, SVG и детали с parent-owned высотой. На ПК детали справа; на телефоне занимают рабочую область, закрытие возвращает карту. Выбор устройства доступен вне SVG, длинные имена переносятся по словам, отсутствует горизонтальный overflow на 1440/390/320 px.
diff --git a/docs/ux-ui-stage-10.md b/docs/ux-ui-stage-10.md
index a5f8d40c..e5223436 100644
--- a/docs/ux-ui-stage-10.md
+++ b/docs/ux-ui-stage-10.md
@@ -293,3 +293,11 @@ Shared контракт и backend observed исходные значения/д
- SVG использует semantic tokens; reduced motion отключает animateMotion. Экспорт сохраняет CSS переменные/фон и сериализуется XMLSerializer как standalone SVG. Nullable UL не выводится строкой null; неизвестный IPsec явно подписан.
- 35 основных + 12 source E2E, TypeScript и lint прошли; ReUI Frame checklist прочитан. Первые регрессионные тесты выбирали скрытые option после добавления селектора: селекторы ограничены SVG, повтор чистый. 6 снимков проверены визуально.
- Далее 10.53: камера по границам объектов и читаемость малой топологии, pointer/touch и пустые состояния; текущие overview снимки ещё показывают слишком мелкий граф. Production build — после этого блока. Реальные RouterOS/JWT/БД не проверены.
+
+
+## Блок 10.53 — камера и читаемость карты (2026-10-01)
+
+- ViewBox учитывает реальное соотношение сторон canvas; начальное вписывание включает устройства, WAN, сервисы и подписи. Оно выполняется один раз на источник и не сбрасывает ручной zoom после обновления API. Кнопка/горячая клавиша вписывают текущие объекты; wheel/drag используют ту же геометрию.
+- Название вынесено под узел; на телефоне размер текста компенсирует SVG масштаб, длинная подпись сокращена при сохранённом полном accessible name/деталях. Страна обозначена ISO при отсутствии площадки и до загрузки внешнего изображения флага. Легенда — доступные раскрываемые обозначения вне SVG.
+- WAN карточка получила Enter/Space и aria-pressed. 2 camera unit + 16 E2E layout/details/URL, TypeScript и lint прошли; после ISO fallback повторены 5 layout E2E, TypeScript и lint. Снимки обновлены.
+- Далее pointer/touch, закрытие деталей с возвратом фокуса, пустой inventory/пустой срез; затем production build и локальная приёмка карты. Реальная сеть не проверена.
diff --git a/docs/ux-ui/screens/network-map-detail-desktop.png b/docs/ux-ui/screens/network-map-detail-desktop.png
index e26a1e02..8e23ef43 100644
Binary files a/docs/ux-ui/screens/network-map-detail-desktop.png and b/docs/ux-ui/screens/network-map-detail-desktop.png differ
diff --git a/docs/ux-ui/screens/network-map-detail-mobile.png b/docs/ux-ui/screens/network-map-detail-mobile.png
index f22e346c..562ef923 100644
Binary files a/docs/ux-ui/screens/network-map-detail-mobile.png and b/docs/ux-ui/screens/network-map-detail-mobile.png differ
diff --git a/docs/ux-ui/screens/network-map-detail-narrow.png b/docs/ux-ui/screens/network-map-detail-narrow.png
index a5be573e..de25e802 100644
Binary files a/docs/ux-ui/screens/network-map-detail-narrow.png and b/docs/ux-ui/screens/network-map-detail-narrow.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-desktop.png b/docs/ux-ui/screens/network-map-overview-desktop.png
index 1af54dfa..26711449 100644
Binary files a/docs/ux-ui/screens/network-map-overview-desktop.png and b/docs/ux-ui/screens/network-map-overview-desktop.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-mobile.png b/docs/ux-ui/screens/network-map-overview-mobile.png
index afd6f9f9..84a4f776 100644
Binary files a/docs/ux-ui/screens/network-map-overview-mobile.png and b/docs/ux-ui/screens/network-map-overview-mobile.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-narrow.png b/docs/ux-ui/screens/network-map-overview-narrow.png
index 28e956d8..5b9dce6e 100644
Binary files a/docs/ux-ui/screens/network-map-overview-narrow.png and b/docs/ux-ui/screens/network-map-overview-narrow.png differ
diff --git a/lib/network-map-camera.test.ts b/lib/network-map-camera.test.ts
new file mode 100644
index 00000000..dc36a747
--- /dev/null
+++ b/lib/network-map-camera.test.ts
@@ -0,0 +1,5 @@
+import {test} from "node:test"
+import assert from "node:assert/strict"
+import {fitNetworkMapCamera} from "./network-map-camera"
+test("camera fits off-world nodes and labels across aspect ratios",()=>{for(const h of [580,1600]){const points=[{x:-200,y:50},{x:1000,y:700}],c=fitNetworkMapCamera(points,1240,h);for(const p of points){assert.ok(p.x>=c.pan.x+119);assert.ok(p.x<=c.pan.x+1240/c.zoom-119);assert.ok(p.y>=c.pan.y+99);assert.ok(p.y<=c.pan.y+h/c.zoom-99)}}})
+test("empty/invalid positions and maximum initial zoom",()=>{assert.deepEqual(fitNetworkMapCamera([],1240,580),{pan:{x:0,y:0},zoom:1});assert.deepEqual(fitNetworkMapCamera([{x:NaN,y:1}],1240,580),{pan:{x:0,y:0},zoom:1});assert.equal(fitNetworkMapCamera([{x:1,y:1}],1240,1600).zoom,3)})
diff --git a/lib/network-map-camera.ts b/lib/network-map-camera.ts
new file mode 100644
index 00000000..3cd9b762
--- /dev/null
+++ b/lib/network-map-camera.ts
@@ -0,0 +1,8 @@
+export interface MapPoint {x:number;y:number}
+export function fitNetworkMapCamera(points:readonly MapPoint[],width:number,height:number) {
+ const valid=points.filter(p=>Number.isFinite(p.x)&&Number.isFinite(p.y))
+ if(!valid.length||width<=0||height<=0)return {pan:{x:0,y:0},zoom:1}
+ const xs=valid.map(p=>p.x),ys=valid.map(p=>p.y),left=Math.min(...xs)-120,right=Math.max(...xs)+120,top=Math.min(...ys)-100,bottom=Math.max(...ys)+100
+ const zoom=Math.max(.2,Math.min(3,width/(right-left),height/(bottom-top)))
+ return {pan:{x:(left+right)/2-width/(2*zoom),y:(top+bottom)/2-height/(2*zoom)},zoom}
+}
diff --git a/tests/e2e/network-map-layout.spec.ts b/tests/e2e/network-map-layout.spec.ts
index ecd12e55..afeaed35 100644
--- a/tests/e2e/network-map-layout.spec.ts
+++ b/tests/e2e/network-map-layout.spec.ts
@@ -44,3 +44,21 @@ test("Карта: reduced motion и самостоятельный SVG эксп
const parsed=await page.evaluate(source=>{const doc=new DOMParser().parseFromString(source,"image/svg+xml");return {errors:doc.querySelectorAll("parsererror").length,width:doc.documentElement.getAttribute("width")}},source)
expect(parsed.errors).toBe(0);expect(parsed.width).toBe("1240")
})
+
+test("Карта: пропорции камеры, ручной масштаб и обновление без сброса",async({page})=>{
+ await page.setViewportSize({width:390,height:900});await page.goto("/network-map")
+ const map=page.getByRole("group",{name:"Интерактивная карта сети"})
+ await expect(page.getByRole("button",{name:/^Устройство Домашний/})).toBeVisible()
+ await expect.poll(async()=>Number((await map.getAttribute("viewBox"))!.split(" ")[2])).toBeLessThan(1240)
+ const camera=await map.evaluate((element:SVGSVGElement)=>{const rect=element.getBoundingClientRect(),box=element.viewBox.baseVal;return {ratio:box.width/box.height,screenRatio:rect.width/rect.height}})
+ expect(camera.ratio).toBeCloseTo(camera.screenRatio,3)
+ await page.getByRole("button",{name:"Увеличить карту",exact:true}).click()
+ const manual=await map.getAttribute("viewBox")
+ await page.getByRole("button",{name:"Обновить",exact:true}).click()
+ await expect(page.getByRole("button",{name:"Обновить",exact:true})).toBeEnabled();await expect(map).toHaveAttribute("viewBox",manual!)
+ await page.getByRole("button",{name:"Вписать карту",exact:true}).click()
+ expect(await map.getAttribute("viewBox")).not.toBe(manual)
+ await page.getByRole("combobox",{name:"Устройство на карте"}).selectOption("1")
+ const wan=page.getByRole("button",{name:"WAN WAN",exact:true});await wan.focus();await wan.press("Enter")
+ await expect(page).toHaveURL(/wan=0/);await expect(wan).toHaveAttribute("aria-pressed","true")
+})