---
name: ops-dashboard-vpn-tab
description: Вкладка VPN — групповые команды pause_all/resume_all/disconnect_all одним перезапуском; в панели нет нативных prompt; статус кэшируется в STATE.vpnLast
metadata: 
  node_type: memory
  type: project
  originSessionId: ca9d2fe6-c078-4428-b2e2-b28e031e1440
  modified: 2026-07-30T08:52:49.187Z
---

Вкладка VPN в ops_dashboard переработана 2026-07-30. Групповые операции — команды `pause_all <minutes>` / `resume_all` / `disconnect_all` в `hy2_admin.py` на origin, по одной перерисовке+перезапуску на всех. `resume_all` поднимает ТОЛЬКО отключённых по паузе; `manual`/`quota_exceeded`/`expired` не трогает. Порядок карточек: в сети → с доступом → выключенные → непривязанный трафик. Диагностика (нагрузка, графики, сертификат) — в свёрнутом `<details data-vpn-diag>`. Статус учётки: «В сети»/«Простаивает»/«Не подключался»/«Пауза»/«Лимит»/«Истёк»/«Выключен» вместо общего сервисного чипа.

**Why:** «Пауза всем» шла циклом по учёткам, а `command_pause` вызывает `install_runtime()` → `systemctl restart hy2`, поэтому 9 учёток = 9 перезапусков и туннель падал у всех девять раз. Модалка настроек закрывалась после каждого действия (`closeModal()` в `vpnAction` был безусловным), поэтому лимит и скорость нельзя было выставить за один заход. Создание пользователя шло через три нативных `prompt`/`confirm` — в Telegram Web App это худший ввод.

**How to apply:** нативных `prompt(` в `server.py` теперь НОЛЬ (единственное намеренное исключение — `confirm()` перед `delete`, см. ниже) — вводить только формой через `showModal`+`fieldGroup`. Любая мутирующая кнопка оборачивается в `vpnBusy(ev, fn)` (блокирует от двойного клика), действия внутри диалога — с `{keepModal:true}` + `refreshVpnSettings()`. Список статуса кэшируется в `STATE.vpnLast` (ставится в `vpnApply`) — использовать его для диалогов, иначе открытие ждёт SSH-рейс 7-9 с. Разрабатывать/проверять новую логику групповых команд сначала на копии `hy2_server.json`/`hy2_users_state.json` с подменённым `install_runtime` (даёт число вызовов без риска), а UI — Playwright с сессионной cookie (см. [[ops-dashboard-session-cookie]]); боевой прогон делать только явно попросив короткую паузу (1 минута), не `0` (бессрочно). Порты панель берёт из `alt_port` в статусе, см. [[hy2-carrier-port-filter]].

**Известное ограничение:** каждый `/api/vpn/status` — SSH-рейс до US-VPS (7-9 с), из-за чего первая отрисовка вкладки медленная. Лечится только демоном с локальным API на origin вместо SSH-на-команду.

**Боевой прогон `pause_all 1` (2026-07-30) подтвердил на живом сервере:** 9 учёток → 1 перезапуск (один PID sing-box), `active_connections` → 0, через минуту авто-возобновление без вмешательства (`enabled=true, paused_until=null` у всех). Владелец переподключился сам; трафик временно ушёл под «unknown», потому что телефон сменил IP — это существовавшая атрибуция по `ip_pins`, не баг паузы.

**Команда `delete <name>`** добавлена в `hy2_admin.py` — безвозвратно убирает учётку из `state["users"]` и снимает её `ip_pins`, один `install_runtime()`; отклоняет несуществующее/некорректное имя ДО перерисовки. Кнопка «🗑 Удалить учётную запись навсегда» в диалоге настроек — единственное место, где оставлен нативный `confirm()`, намеренно: действие необратимо (пароль, ссылка-подписка, история трафика теряются), и здесь confirm — последний барьер, а не забытый диалог.

**«Непривязанный трафик» убран из основной сетки (2026-07-30).** Это НЕ дыра в безопасности — трафик уже авторизован конкретным паролем, просто Clash API sing-box не отдаёт дашборду, каким именно (только IP), а с 2+ включёнными учётками автоматическая атрибуция по IP невозможна без ручной `pin_ip`. Владелец не может надёжно угадать, чей это IP — карточка перенесена в свёрнутый `<details data-vpn-diag>`, кнопка «Привязать →» осталась там же на случай, если IP когда-то станет известен точно.
