Files
kaskad_platform/CHANGELOG.md
andrey271192 bee2bfb955 fix: замер скорости выходов, дёрганье клиентов, журнал, уборка пиров
Замер мерил не выход, а зеркало: tele2 по HTTPS не отвечает (000), следом шёл
proof.ovh.net со своими ~1.6 Mbps — и здоровые выходы уезжали в degraded.
Проверено вручную: ovh 193-259 КБ/с против cachefly 8.8-11 МБ/с на тех же
выходах. Теперь быстрые зеркала первыми и берётся максимум из проб.

probe_mbps стал медианой окна из 5 замеров; выход из degraded требует двух
хороших подряд и запаса над порогом. Расчёт вынесен в apply_probe_sample().

rebalance_auto_exits получил выдержку 30 минут на добровольный переезд —
отказ текущего выхода двигает клиента по-прежнему сразу.

События панели пишут время, лимит журнала 100 -> 400, в ленте видна метка.

prune_orphan_peers() снимает пиров, которых нет ни у одного клиента: ключ
удалённого клиента продолжал пускать бывшего владельца в туннель.

Источник этих пиров — собственные тесты: прогон на sber2 добавлял пиров в
живой kaskad-entry (105 -> 210 за два прогона) и писал снапшоты в боевой
data/backups. conftest.py выставляет временный KASKAD_DATA_DIR до импорта
server.main, глушит сетевые команды и изолирует автодетект интерфейсов —
заодно этим чинятся два теста, которые падали только на боевой машине.

Тесты: 88 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:56:44 +03:00

72 KiB
Raw Permalink Blame History

Changelog

2026-08-26 (вечер) — починены замер скорости, дёрганье клиентов, журнал и уборка пиров

  • Корневая причина «деградаций» найдена: мерялась скорость зеркала, а не выхода. EXIT_PROBE_URLS перебирался по порядку и брался первый ответивший. speedtest.tele2.net по HTTPS не отвечает вовсе (000), следующим стоял proof.ovh.net, который сам отдаёт ~1.6 Mbps — и здоровые выходы уезжали в degraded с записью «1.6 Mbps», хотя тот же канал через cachefly даёт 80 Mbps. Проверено вручную с sber2 по трём выходам: ovh 193259 КБ/с против cachefly 8.811 МБ/с. Теперь быстрые зеркала первыми (cachefly, speed.cloudflare.com), ovh — резерв, и берётся максимум из проб: меряем ёмкость канала, а не тормоза чужого сервера. Ранний выход при EXIT_PROBE_GOOD_ENOUGH_MBPS = 25.
  • Замер сглажен, выход из degraded асимметричен входу. probe_mbps — медиана последних EXIT_PROBE_WINDOW = 5 замеров, а не последнее значение. В degraded выход уходит по-прежнему после двух плохих подряд, но возвращается только после двух хороших подряд И с запасом над порогом (EXIT_PROBE_RECOVER_MBPS = 18). Расчёт вынесен в apply_probe_sample()его можно тестировать без фонового цикла.
  • Клиенты больше не мечутся между выходами. В rebalance_auto_exits добавлена выдержка EXIT_MOVE_COOLDOWN = 1800: добровольный переезд (нашёлся выход быстрее) не чаще раза в полчаса. Отказ текущего выхода (offline/degraded) по-прежнему двигает клиента немедленно. Полностью бесшовной смена выхода быть не может — меняется внешний IP, TCP-сессии рвутся; поэтому лечение в том, чтобы не дёргать клиента без причины.
  • События получили время. add_event пишет at в локальной зоне сервера с offset, лимит журнала поднят со 100 до 400 записей (сотню выметало за час), API отдаёт 60 последних, в ленте панели время показывается перед уровнем.
  • Пиры без клиента снимаются автоматически. prune_orphan_peers() сверяет пиров на kaskad-entry/kaskad-awg/kaskad-awg2 с ключами всех клиентов и снимает чужие, о чём пишет в журнал. Вызывается в цикле монитора.
  • Найден источник тех самых «осиротевших» пиров: собственные тесты. Прогон pytest на sber2 добавлял WG-пиров прямо в живой kaskad-entry — за два прогона 105 → 210. Тесты также писали снапшоты в боевой data/backups (там лежали чужие файлы аж с 13.06). Добавлен tests/conftest.py: временный KASKAD_DATA_DIR выставляется ДО импорта server.main, а subprocess.run для wg/awg/ip/nft/iptables/ipset/systemctl/sysctl подменяется заглушкой. Проверено: прогон больше не меняет число пиров (210 → 210).
  • Два «вечно красных» теста были не багом кода, а зависимостью от машины. sync_local_entry_public_keys определяет установленный AmneziaWG по живым интерфейсам kaskad-awg/kaskad-awg2; на sber2 они есть, поэтому тесты, ожидающие отказ «AWG не установлен», падали именно там. Autouse-фикстура в conftest изолирует автодетект. Итог: 87 passed, 0 failed.
  • Добавлены тесты: медиана и асимметричное восстановление, выдержка переезда, снятие только чужих пиров, время в событии, выбор лучшего зеркала при замере.

2026-08-26 — AIVPN удалён, репозиторий синхронизирован с продом

  • Репозиторий отставал от боевого сервера с 22.06.2026. Каталог /opt/kaskad_platform на sber2 не был git-репозиторием вовсе, правки жили только на сервере. Прод зафиксирован коммитом «sync: снимок боевого сервера sber2 перед удалением AIVPN» — это точка отката.
  • Удалён протокол AIVPN (по решению владельца — не нужен). Вырезано из server/main.py: константы, Literal-значения в InstallProtocolIn/ClientIn/AccountPeerIn, поля состояния aivpn_installed/aivpn_port/aivpn_last_error, функции aivpn_server_ip, aivpn_client_key, install_aivpn_server, mint_aivpn_key, ставший мёртвым run_entry_shell, роуты /aivpn/agent и /aivpn/install.sh вместе с роутерным установщиком, ветки в выдаче конфига/share/download/QR, а из UI — опции в двух формах, чип и кнопка на карточке RU входа, installEntryAivpn, лейбл протокола. Итого 7 функций верхнего уровня, 0 упоминаний aivpn в коде.
  • На sber2 снято: служба kaskad-aivpn.service, бинарь /usr/local/bin/aivpn-server, /etc/aivpn, /var/lib/aivpn, nft-таблицы kaskad_aivpn и aivpn, TUN aivpn0, ip rules from 10.93.0.3/10.93.0.6 lookup 200, каталог static/ с 10-мегабайтным клиентским бинарём. Всё удалённое сложено в /root/aivpn-removed-20260826.tar.gz (6.4 МБ) — на случай отката.
  • Удалены два клиента на этом протоколеkeenetic_home и rt0. Оба не использовались: последний хендшейк 22.06.2026, aivpn0 за всё время принял 0 байт (RX=0, TX=33 КБ). Бэкапы состояния: data/state-before-aivpn-removal-20260826.json и state.json.bak-aivpn-*.
  • Проверки: py_compile ok; тесты 81 passed, 2 failed. Оба падения существовали и ДО правки (проверено прогоном на исходном main.py): test_amneziawg_requires_amnezia_entry_mode и test_install_awg_protocol_enables_amneziawg_clients ждут отказа 400 при создании клиента AmneziaWG 2.0 на входе с v1-инсталляцией, а код отвечает 200. Это отдельный расхождение тестов и кода, к AIVPN отношения не имеет.
  • После деплоя: панель active, /api/health ok, /aivpn/install.sh отдаёт 404, xray с Reality жив, три kx* туннеля на месте.

2026-06-19 — Exit quality monitor + degraded failover/load-balance

  • Исправлена корневая причина недельных “провалов/тормозов”: check_exit() раньше проверял только liveness (ping/TCP/SSH). Медленный exit оставался online, failover не срабатывал, auto-клиенты могли висеть на slow-but-pingable сервере.
  • Добавлен active throughput probe выходов: отдельный monitor thread каждые 300с меряет реальную downstream-скорость через cascade route table каждого exit (probe_mbps, probe_ts, probe_fail_streak, degraded). Probe идёт через source-based route; source-IP не дублируется на kx-интерфейсах, чтобы не ломать routing.
  • failover теперь считает online + degraded плохим состоянием и переводит primary на здоровый exit.
  • load_balance теперь учитывает реальную ёмкость канала: быстрый exit получает больше auto-клиентов, но не всех. Пример: 300 Mbps против 100 Mbps стремится к ~3:1, а не 20:0. Degraded exit исключается из пула.
  • Добавлен rebalance_auto_exits(): sticky auto-клиенты переезжают только если текущий exit offline/degraded или сильно хуже здорового, чтобы не было постоянного flapping и обрывов видео.
  • В UI у серверов в “вес нагрузки” теперь виден реальный замер канала (замер X Mbps) и метка degraded.
  • Default client MTU снижен 1420 -> 1320 для запаса под двойную инкапсуляцию client WG/AWG + cascade kx.
  • Проверки: py_compile ok, embedded JS node --check ok, 83 passed.

2026-06-11 (ночь-2) — VLESS лимиты, provision-кнопка, persistence, mieru removal

  • Лимиты клиента = только устройства + дата (по запросу): поле device_limit (0=без лимита) рядом с expires_at. В форме создания + модалке «Данные». Enforcement (мягкий, безопасный): xray access-log (email=uuid) на каждом Reality-входе, монитор каждые 60с считает уникальные source-IP на uuid → client.activity.online_devices, алерт при превышении (без авто-кика). Проверено live: коннект логируется, online_devices=1.
  • «Включить Reality» кнопка на RU-входе: POST /api/entry_servers/{id}/reality ставит xray, генерит Reality-keypair (идемпотентно — сохраняет ключи если уже есть, конфиги клиентов не ломаются), порт :443 (или :8443 если занят), source-IP/route + drop-in persistence, store + sync. Проверено: повторный provision сохранил pubkey.
  • Routing persistence: drop-in xray.service.d/cascade.conf на обоих входах re-добавляет alias+ip rule при (пере)старте xray → reboot-safe.
  • mieru удалён из API (Literal клиента + install), UI (опции + кнопка → Reality), тестов (5 шт). mita-сервисы отключены. Dormant runtime-функции остались (недостижимы) — полное удаление кода = follow-up.

2026-06-11 (ночь) — VLESS-Reality + sing-box подписка (обход DPI для телефона)

  • Новый протокол vless_reality — DPI-устойчивый путь для телефона. Reality маскирует под реальный TLS-сайт SberCloud (obs.ru-moscow-1.hc.sbercloud.ru). Ключевое: входы физически на AS208677 (SberCloud) → real-IP target в той же ASN → SNI↔IP корреляция чистая (DPI видит TLS к SberCloud-домену с SberCloud-IP = норм облачный трафик). Проверено: :443 отдаёт настоящий cert SberCloud (CN obs…, GlobalSign).
  • Один импорт, авто-failover: /api/clients/{id}/subscription → sing-box config с ДВУМЯ VLESS-outbound (sber_2 + sber_1) + urltest (30s). ?format=links → vless:// ссылки, /subscription_qr → QR. Импорт в Karing/sing-box. Проверено: egress зарубеж через каскад, failover sber_1↔sber_2 за ~40с при падении входа.
  • Модель: клиент vless_reality = uuid-личность (без WG ключей/tunnel). Запись entry.reality (port/keys/sni/short_id/source_ip/route_table), privkey вырезан из /api/status. sync_entry_xray строит xray VLESS+Reality inbound (все enabled uuid) + freedom outbound на cascade-source IP, пушит на вход (локально/SSH) + reload xray. apply/create/delete/failover завязаны: клиент публикуется на primary + (опц.) backup Reality-вход; disable/delete снимает uuid с обоих. Failover VLESS переиспользует backup_entry_id (без WG backup-подсети).
  • DNS в подписке: домены входов резолвятся DIRECT (bootstrap, без петли), реальный трафик — через прокси (без DNS-leak/РФ-poison).
  • Инфра: xray-core на обоих входах :443. На sber_1 443 был занят nginx (keenetichome.ru) → перенёс сайт на :80 (backup /root/nginx-bk-reality/, nginx -t + rollback, не сломал hysteria/amnezia/caddy:8443). Каскад-routing VLESS: alias 10.77.0.240/10.87.0.240 → exit-таблица (live; платформа re-применяет в sync_entry_xray).
  • UI: vless_reality в dropdown протокола; в failover-баре для vless — выбор 2-го входа + кнопки Подписка/Ссылки/Скачать/QR. Опция mieru убрана из создания.
  • Cleanup: mita (mieru server), warp-svc, kaskad-web отключены на входах (systemctl disable --now, обратимо). 85 passed, задеплоено.
  • TODO: provision-эндпоинт «Включить Reality» на входе (сейчас seed в state вручную); persist cascade-routing (alias/ip rule live — теряются при reboot, re-применяются при sync); полное удаление mieru-кода.

2026-06-11 (вечер-3) — P2: dual-entry failover в панели (per-client backup)

  • Модель: client.backup_entry_id (пусто = обычный 1-вход, задан = failover). Backup-подсеть зеркалит primary: 10.77→10.87 / 78→88 / 79→89 (тот же host-октет → уникально, без overlap AllowedIPs на exit). Backup использует свою keypair (на Keenetic 2 интерфейса = 2 ключа — как в проверенной ручной настройке).
  • API: POST /api/clients/{id}/failover {primary_entry_id?, backup_entry_id} — валидирует, что backup это cascade-host, минтит backup-ключ, кладёт peer на backup-вход; пустой backup → снимает peer. Конфиг-эндпоинты (/config,/share,/qr,/download) принимают ?variant=primary|backup.
  • Sync на оба входа: apply_client_runtime_route теперь зеркалит каждого клиента на backup-вход (все call-sites). Disable/delete снимает peer с ОБОИХ входов — чинит «удалил клиента, а он жив по второму серверу».
  • UI: failover-бар в модалке конфига клиента (переключатель Primary/Backup + выбор backup-входа + Сохранить).
  • Проверено вживую на sber_1: включение failover регистрирует backup-peer (n1U7…@10.87.0.3), удаление — снимает. 83 passed, JS node --check ok, задеплоено SERVER_IP, sber_1 помечен cascade_host.
  • Совместимость с ручной настройкой дома: домашний роутер настроен вручную (nwg4 ключ 9SUuY5@10.87.0.2), панель про него не знает. Если включить P2-failover на его primary-клиенте (10.77.0.2 → backup 10.87.0.2) — будет коллизия IP с ручным peer. Перед миграцией снять ручной peer (wg set kaskad-entry peer 9SUuY5… remove на sber_1) и переимпортить сгенерённый backup-конфиг на роутер.

2026-06-11 (вечер-2) — dual-RU failover ПРОВЕРЕН на живом Keenetic

  • Реальный роутер (Keenetic Netcraze, Entware /opt/bin/wg, ndmc). Настроен 2-й WG-интерфейс Wireguard4 "sber-backup" → globalsecure.ru:51820 (sber_1), addr 10.87.0.2/32, reuse ключа простаивавшего интерфейса (snapshot старого конфига сохранён для отката). На sber_1 зарегистрирован peer роутера (wg set kaskad-entry peer <pub> allowed-ips 10.87.0.2/32).
  • e2e с роутера: ping 8.8.8.8 через nwg4 — 0% loss; egress IP SERVER_IP (Швеция exit). Реальный Keenetic получает интернет через резервный RU-вход.
  • Failover-цепочка: добавлен permit global Wireguard4 в ip policy HydraRoute (порядок: AM → sber/sber_2 → sber-backup/sber_1), конфиг сохранён.
  • Тест отказа: при выключенном Wireguard5 (sber_2 primary) backup Wireguard4 (sber_1) самостоятельно держал каскад — 0% loss, egress SERVER_IP. Primary восстановлен.
  • Ключевые синтаксис-заметки Keenetic ndmc: SSH root падает в busybox sh (не ndm CLI); конфиг через ndmc -c "..."; WG-peer задаётся ТОЛЬКО после создания узла peer отдельной командой (interface WgN wireguard peer <KEY>), затем ... endpoint host:51820 (двоеточие), ... allow-ips 0.0.0.0 0.0.0.0, ... connect; комбинированная строка peer <KEY> endpoint ... = argument parse error; интерактивный stdin ndmc по non-tty не работает.
  • Живучесть (P2/persist): peer роутера на sber_1 и 2-й peer/маршруты на exit добавлены LIVE (не в kaskad-entry.conf/kaskad-exit.conf) → теряются при перезагрузке sber_1/exit. Платформа должна синхронизировать (P2 dual-entry peer model). Пока — re-make_cascade + re-wg set после reboot.

2026-06-11 (вечер) — dual-RU failover P0 РЕШЁН: sber_1 e2e зелёный

  • Root cause найден и устранён. sber_1 → exit: клиент подключался, инета не было. Причина: при регистрации sber_1 как 2-го peer на exit код звал wg set kaskad-exit peer ... allowed-ips 10.87/88/89 — это ставит cryptokey-routing wg, но НЕ добавляет kernel-route. Без ip route ... dev kaskad-exit обратные пакеты un-NAT-ились в 10.8x.y и уходили по default-маршруту (eth0), а не в туннель → SYN доходил до exit, ответ не возвращался. (wg-quick добавляет такие маршруты для peer из .conf; живому 2-му peer нужно явно.)
  • Фикс (код sync_remote_entry_cascade): после wg set ... allowed-ips на каждом exit добавлять ip route replace 10.87.0.0/24 dev kaskad-exit scope link (+ .88/.89). Идемпотентно. Применено live на все 3 exit + зашито в sync.
  • e2e ДОКАЗАН (netns 10.87.0.50 на sber_1 → каждый exit, TCP+TLS 1.1.1.1/cdn-cgi/trace): table 200→Швеция ip=SERVER_IP, 202→test ip=SERVER_IP, 204→SGA_3 ip=SERVER_IP. ICMP 8.8.8.8 0% loss, return-path подтверждён по egress-IP. sber_2 (primary) не тронут — у exit оба peer целы (sber_2: 10.77/78/79, sber_1: 10.87/88/89), без overlap.
  • Проверки: 80 passed, deploy SERVER_IP ok, /api/health ok, повторный sync_remote_entry_cascade из задеплоенного кода → все 3 exit отдают 3 backup-маршрута (идемпотентно).
  • СЛЕДУЮЩЕЕ (P2): dual-entry peer model — синхронизировать peer клиента на ОБА входа + 2 конфига Keenetic (primary pcastrosole.ru / backup globalsecure.ru) + UI «Keenetic failover». Известная мелочь P1: на sber_1 ip rule from 10.87/88/89 lookup <первый exit table> = coarse (все backup-клиенты на первый exit), per-client refine ещё не реализован — для failover MVP приемлемо.
  • Примечание о живучести: 2-й peer + маршруты на exit живут до reboot самого exit (добавляются live, не в kaskad-exit.conf). После reboot exit — повторный make_cascade. Зашить в генератор kaskad-exit.conf — отдельная задача.

2026-06-11 — dual-RU failover (option A): design VALIDATED live

  • Handoff refreshed after c434ee5: sync_remote_entry_cascade() + POST /api/entry_servers/{id}/make_cascade уже добавлены и задеплоены; sber_1 получил kx-туннели/backup subnets/RU-direct/2-й peer на exits, но реальный e2e через sber_1 пока НЕ подтверждён. Последний честный статус: netns-клиент через sber_1 даёт SYN на kaskad-exit, дальше нет egress/return. Следующий чат должен продолжить с tcpdump kaskad-exit+eth0, FORWARD/NAT counters, Docker DROP/rule-order/rp_filter/MASQ диагностики. docs/claude-prompt.md переписан под это место остановки. Секреты/доступы роутера в docs не внесены.

  • Option A addressing PROVEN end-to-end на живом стенде (sber_1 → test exit), prod не тронут (sber_2 цел весь процесс). Выбран вариант: отдельные client-подсети для резервного входа (sber_1 = 10.87.0.0/24 WG / 10.88 AWG / 10.89 AWG2), чтобы у exit не было overlap AllowedIPs (иначе возврат идёт только одному RU = «online, нет инета»). Рецепт (валидирован tcpdump на exit — двунаправленный трафик, MASQ, conntrack-reverse):

    • на резервном RU (sber_1) для каждого exit: WG-интерфейс kx* (PrivateKey = kaskad-entry.key входа, Address = 10.210.<table>.1/30, peer = exit pubkey, Endpoint = exit:ListenPort (не 51820! у test это 5182), AllowedIPs 0.0.0.0/0, Table=off), ip route default dev kx table <N>, FORWARD-правила (-i kaskad-entry -o kx, return RELATED,ESTABLISHED), ip rule from <client-net> lookup <N>, rp_filter=2 (loose; иначе политик-роут возврат дропается).
    • на каждом exit: добавить sber_1 как 2-й peer kaskad-exit с НЕпересекающимися AllowedIPs = 10.87/10.88/10.89 + 10.210.<table>.1/32, 2-й address 10.210.<table>.2/30, MASQ -s 10.87/88/89. Делать через wg set ... peer ... allowed-ips + ip addr add (live, НЕ wg setconf — не сбросить sber_2 peer).
    • порт берётся из exit.endpoint_port (= ListenPort kaskad-exit), не хардкодить.
    • exit держит 2 peer (sber_2: 10.77/78/79, sber_1: 10.87/88/89) без конфликта возврата.
  • Диагностика (P0): sber_2 (176) — полный каскад (3 kx: Швеция/test/SGA_3, ipset RU 8822). sber_1 (213) — голый (kaskad-entry up, 0 kx, ipset=0). 3 exit online (Польша удалён). Доступ к sber_1 — через metrics-ключ панели с sber_2.

  • СЛЕДУЮЩЕЕ (код): sync_remote_entry_cascade(entry) (идемпотентно строит kx ко всем exit + 2-й peer на exit + RU-direct ipset/table150 на sber_1), модель dual-entry peer (backup_entry_ids + параллельные подсети 10.87/88/89), 2 конфига для Keenetic (primary sber_2 / backup sber_1), UI «Keenetic failover», тесты. Полный рецепт выше.

  • mieru phone «нет интернета» — РЕШЕНО (несколько багов подряд): (1) dead-домен ru1.pcastrosole.ru (нет DNS) → переключил оба входа на apex (pcastrosole.ru / globalsecure.ru, оба резолвятся); (2) Karing/sing-box mieru-конфиг имел неверные поля transport/multiplex → исправлено на protocol/multiplexing (4c5f0fe); (3) mita apply config НЕ перезаписывает пароль существующего юзера → пересозданный клиент держал старый ключ → sync теперь удаляет+пересоздаёт юзеров с изменённым паролем (1194325); (4) ГЛАВНОЕ — mita (mieru-сервер) ЛЕЖАЛ (FATAL «no user found» после sync, очистившего юзеров; не стартует без юзеров, а apply нужен запущенный демон — дедлок) → восстановил вручную + добавил mita_self_heal(): при неудачном старте сбрасывает store, стартует пусто, re-apply юзеров, рестарт — больше mieru-сервер не уляжется (236c42f). Проверено: mita active, listening :2999, apple LastActive с данными. Вывод для iOS+DPI: Karing+mieru рабочий путь (per-app routing), но AmneziaWG 2.0 (apple_a, проверен e2e) надёжнее.

  • Резервные выходы (c162671): per-exit флаг reserve. exit_pool() отдаёт онлайн-активные, резервные — только если активных онлайн нет. UI: селект «Активный/Только резерв» в форме + бейдж «резерв» + кнопка-тоггл /api/exit_servers/{id}/reserve.

  • Коллизии слотов exit (f3afd3a,21fbba6): route_table + ru_tunnel_ip аллоцировались по len(exits) → после удалить+добавить два exit делили table 201 и 10.200.2.1/30 → перезапись default dev kx + дубль connected-route → возврат второго exit перехватывался (online без инета). Фикс: next_exit_slot() (first-free) во всех 3 точках + дедуп route_table в normalize_state. Live: Швеция переназначена на 10.200.4.x + rebuild. ВАЖНО: rebuild kx через restart флашит default dev kx table N → нужен reconcile после.

  • WARP накрывает ВСЕ подсети (a7bbdbb): client_nets строился из wg_settings.network(10.77)+пустой awg2 → AWG/AWG2 шли мимо WARP. Теперь из all_client_networks() (10.77/10.78/10.79). Live SGA_3 переприменён — все 3 в ip rule+MASQ, WARP+ connected.

  • WARP+ ключ запоминается (fbfebfe): state.warp_license_key (секрет, в UI только bool warp_license_set). Ввёл раз — дальше поле пустое = берётся сохранённый.

  • Ping вместо «—» (143da3d): ICMP блокируется на большинстве VPS → tcp_latency() меряет RTT TCP-рукопожатия к SSH-порту как ping. Live: test 54ms (ICMP), SGA_3 40ms / Швеция 23ms (TCP).

  • UI redesign + lag/perf фиксы (ранее в сессии): дашборд под Apple-grade архив (серверы-карточки, клиенты-аватары); /api/status 171КБ→15КБ (не слать 8600 RU CIDR); коммент-клир; quick-action 2→1 fetch; bench-test всех кнопок — 0 JS-ошибок.

  • СЛЕДУЮЩАЯ ЗАДАЧА (option A — failover роутера между 2 RU входами): sber_1 (SERVER_IP) сейчас «голый» вход (kaskad-entry поднят, но 0 kx-туннелей к выходам, 0 маршрутов, 0 ipset, платформа не запущена) → переключение туда = подключился без инета. Нужно: (1) поднять полный каскад на sber_1 (kx-туннели ко всем выходам + ip rules + ipset RU-direct), (2) платформа должна класть peer клиента на ОБА входа + отдавать 2 конфига (endpoint sber_2 и sber_1), (3) Keenetic dual-interface failover (приоритеты + ping). Детали в docs/claude-prompt.md.

  • Handoff для нового Claude-чата обновлён: docs/claude-prompt.md переписан под текущий objective: real Keenetic failover через 2 RU entry, безопасный план работ, текущие ограничения, проверки, deploy pattern и критерии done. docs/notes.md дополнен задачей и состоянием.

2026-06-09 (ночь) — каскад: AWG-клиенты + коллизии слотов exit

  • AWG-Legacy / AWG2 клиенты были online, но без интернета. Каскадный exit-конфиг знал только 10.77.0.0/24 (WireGuard): peer AllowedIPs + MASQUERADE на exit покрывали лишь WG, поэтому пакеты от 10.78.0.2 (AWG Legacy) / 10.79.x (AWG2) WireGuard на exit ОТБРАСЫВАЛ (src не в AllowedIPs) и не маскировал. Фикс (00bcf75 + live на всех 3 exit): exit-конфиг строится из all_client_networks() (10.77/10.78/10.79) — AllowedIPs, MASQUERADE, return-rule. Проверено tcpdump: AWG-трафик (10.78) ходит двунаправленно через Польшу и Швецию.
  • Коллизия слотов exit (route_table + ru_tunnel_ip). Аллокация по len(exit_servers) → после удаления одного exit и добавления другого слот переиспользовался: два exit (Польша+Швеция) оказались на одной route_table=201 И одном cascade-адресе 10.200.2.1/30. Итог: перезапись default dev kx в общей таблице + дубль connected-route → возвратный трафик второго exit перехватывался (online, но без интернета). Фиксы: (1) normalize_state дедуп route_table (first-free, self-heal на каждый load), (2) next_exit_slot() — first-free слот для route_table/route_interface/ru_tunnel_ip во всех 3 точках создания, (3) live: Швеция переназначена на 10.200.4.x + rebuild kx + repush exit-conf. Проверено tcpdump: оба exit отдают возврат.
  • RU-direct: добавлен статус-бейдж (АКТИВЕН · N маршрутов / включён · нет маршрутов / выключен) — раньше галка ставилась, но было непонятно, применилось ли.
  • Диагностика платформы (что это: Клиент → RU WG/AWG/Mieru вход → US/foreign exit → интернет; RU-трафик можно оставлять напрямую через RU = table 150): 2 RU входа (sber_2 primary, sber_1), 3 exit (Польша/Швеция/test, все online, уникальные слоты), 5 клиентов (3 WG, 1 AWG Legacy, 1 Mieru proxy), RU-direct активен (8619 CIDR). 78 passed, всё задеплоено SERVER_IP.

2026-06-09

  • Дашборд переведён на Apple-grade дизайн из архива (handoff bundle). Серверы: таблица → карточные сетки (RU входы / Выходные узлы) с флагом, role-pill, статусом, 4 stat-блока с meter, traffic-strip, service-чипами (вход) / WARP-блоком (выход); все кнопки действий и инлайн-редакторы сохранены. Add-формы → 2-кол сетка. Клиенты: цветные аватары (хеш по id), peers-badge, traffic/expiry-пилюли; раскрытие — 3 карточки (Маршрут/Трафик/Дата) + Данные/Peers на всю ширину + peer-карточки. Остальные вкладки (Обзор/RU-direct/Настройки/Биллинг/Конфиги/топбар/KPI/табы) уже соответствовали архиву. CSS scoped под #servers/#clients, хендлеры не тронуты. Проверено вживую через preview по всем вкладкам.
  • Клиент: в мета-строке вместо account-xxxx показывается комментарий, если задан (id уходит в tooltip).
  • Фикс: удаление комментария не сохранялосьsaveAccountInline собирал payload как comment?.value || client.comment, пустое поле падало в старое значение. Теперь берётся литеральное значение инпута → очистка работает (так же phone/address/notes/name).
  • Фикс лагов на кнопках клиентов ($/колокольчик/вкл-выкл): хендлеры делали POST (который УЖЕ возвращает новый sanitized-state) + второй полный load() (ещё один GET /api/status). Добавлен applyServerState() — применяет ответ POST напрямую, один fetch вместо двух (~58 мс).
  • Фикс общего тормоза: /api/status весил 171 КБdirect_routing.geo_expanded_cidrs + resolved_cidrs (~8600 RU CIDR каждый) сериализовались в КАЖДЫЙ ответ status (поллинг 30с + каждое действие). sanitize_direct_routing() шлёт только counts (geo_expanded_count/resolved_count); рантайм-роутинг читает полные массивы из raw state. Ответ 171 КБ → 15 КБ (11×). 78 passed, деплой SERVER_IP.
  • Mieru/Karing: исправлен импорт Mieru proxy в Karing iOS. Старый QR/Copy отдавал mierus:// или человекочитаемый blob, Karing показывал «Нет доступных серверов». Добавлен /api/clients/{id}/share: для mieru_proxy отдаёт чистый sing-box/Mieru JSON (outbounds[type=mieru]) без комментариев, для SOCKS5 — URI, для WG/AWG — обычный конфиг. Кнопки Копировать и QR теперь используют /share; полный Конфиг оставлен для админа и содержит отдельный блок Karing/sing-box JSON + Mieru CLI. Проверено: 78 passed, prod smoke /share/qr/config ok.
  • RU direct: geosite:<category> теперь тянет реальную категорию из v2fly domain-list-community (например geosite:youtube → youtube.com, googlevideo.com, ggpht, ytimg и т.д. через include:), а не один общий antifilter-список на все токены (был баг: geosite:youtube и geosite:ru оба возвращали весь 2.8M-список). Алиас geosite:rucategory-ru. Домены резолвятся в IP (cap GEOSITE_RESOLVE_LIMIT) и попадают в ipset → table 150. Проверено вживую: geosite:youtube = 178 доменов, IP youtube.com попал в ipset → выходит напрямую.
  • WARP: тир (free/WARP+) теперь определяется по реальному Cloudflare trace (warp=plus→WARP+, warp=on→free), а не по запрошенному режиму — мёртвый WARP+ license честно показывает «WARP free». Бейдж и кнопка «WARP active» показывают тир. SSH-поля в форме WARP свёрнуты в <details> (по умолчанию используется сохранённый SSH exit).

2026-06-08

  • RU direct: поддержка GeoIP-токенов. В поле GeoIP можно написать geoip:ru (или просто ru) — платформа тянет агрегированный список CIDR страны с ipdeny.com (RU ≈ 8600 подсетей) при сохранении (кеш 24ч), плюс antifilter[:subnet|:ip] и geosite:*. Токены разворачиваются один раз при сохранении (не в hot-path). Маршрутизация переписана с «одно ip rule на каждый CIDR на клиента» (тысячи subprocess каждые 60с — не тянет страну) на ipset + fwmark: один ipset kaskad_ru (bulk ipset restore), одно mangle-правило MARK на подсеть клиента, одно ip rule fwmark 0x96 lookup 150. O(1) правил независимо от размера списка; RU-трафик уходит напрямую через RU box, остальное — в exit-таблицу. ipset добавлен в installer RU входа. GeoSite-домены резолвятся с лимитом (GEOSITE_RESOLVE_LIMIT). 76 passed.

  • UI: SSH-поля (пароль/логин/порт/private key) больше не подхватывают браузерный autofill (autocomplete="new-password"/"off"). Chrome подставлял чужой сохранённый пароль в SSH-поле → ложный «Permission denied» при добавлении/провижене exit (корень проблемы добавления AMS-WARP через UI; через API всё проходило). Удалён мёртвый ExitWarpIn.excluded_routes и textarea (wgcf-WARP хардкодит split-tunnel, старый ручной exclude-list не читался); note формы WARP переписан на объяснение split-tunnel. 74 passed.

  • AMS-WARP (exit-80324539, SERVER_IP) валидирован: kx3-туннель RU→exit поднят (lazy-создание при назначении клиента, не баг), WARP включён заново через панель → connected, client egress = Cloudflare 104.28.219.137, egress бокса прямой, SSH жив (brick-proof подтверждён). Return-rule добавлен в live-confs AMS-WARP/US_фин.

  • Починены 2 бага каскада, из-за которых клиент роутера (test 10.77.0.2, роутер ROUTER_IP) не выходил в интернет через exit, хотя все туннели роутер→RU→exit были подняты (свежие handshakes). Диагностика по счётчикам wg показала асимметрию: RU→Польша (kx2) 6.29 KiB received / 2.47 MiB sent, т.е. трафик клиента уходил на exit, но обратно почти ничего не возвращалось. Баг 1 (exit, return-path): на exit-боксе Docker переключает политику цепочки FORWARD в DROP, а conf kaskad-exit разрешал только исходящий форвард из туннеля (-i %i). Обратные пакеты (интернет→клиент, -o kaskad-exit) падали в default DROP → клиент не получал ответы. Добавлено правило возврата -A FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT. Баг 2 (RU entry, двойной MASQUERADE): на RU входе правило -s 10.77.0.0/24 -j MASQUERADE было без привязки к интерфейсу и перезаписывало source клиента при заходе в каскадный туннель kx+ (на адрес kx, напр. 10.200.2.1); exit маскарадит только подсеть 10.77.0.0/24, поэтому пакеты уходили в интернет с приватным source и ответы не возвращались. MASQ на RU входе ограничен ! -o kx+ (маскарадим только прямой WAN-выход, не каскад). Исправлено в коде (генераторы kaskad-entry/kaskad-exit/kaskad-awg confs в main.py + display-шаблоны wireguard.py), в live-confs на RU+Польша (durable после рестарта wg), и runtime на боксах. Проверено: tcpdump на kx2 показал живой двунаправленный трафик клиента 10.77.0.2↔Google, счётчики стали симметричными и выросли до 175 MiB received. Тест test_cascade_configs_are_brick_proof_and_return_path_safe. 74 passed, деплой SERVER_IP.

  • WARP brick-proof проверен ВЖИВУЮ через панель (код 1a40ea5). Тест-бокс SERVER_IP (Ubuntu 24.04, Amsterdam): exit AMS-WARP добавлен через API панели, кнопка WARP отработала новый wgcf-скрипт. Результат: panel WARP status=connected, клиентская подсеть 10.77.0.0/24 выходит через Cloudflare (104.28.219.137), egress самого бокса остался SERVER_IP, SSH живой — кирпича нет. Дополнительно: «Permission denied» в UI при добавлении exit оказался браузерным autofill (неверный пароль в поле SSH), а не багом — provision через API с правильным паролем прошёл (HTTP 200). На бокс залит metrics-ключ панели → управление по ключу. Восстановлен ранее закирпиченный SERVER_IP (provider console → SystemRescue/findroot → warp-cli disconnect + systemctl disable warp-svc); 3x-ui/nginx/каскад живы.

  • WARP переписан на brick-proof split-tunnel (заменяет опасный warp-cli mode warp full-tunnel, который захватывал egress бокса и убивал SSH+каскад). Теперь WARP = отдельный WireGuard-интерфейс warp из wgcf-профиля с Table = off: дефолтный маршрут бокса (SSH, каскад RU↔exit, UDP до WARP-эндпоинта, apt) НИКОГДА не уходит в Cloudflare. Через WARP policy-route'ятся ТОЛЬКО подсети форвардящихся клиентов (wg_settings.network + awg2), ip rule from <client_net> lookup 51900, MASQ -o warp. Даже если WARP полностью сломан, бокс остаётся доступен — захват egress невозможен by design. off/uninstall = wg-quick down warp + чистка. Убран старый exclude-list (хрупкий: пропустишь один маршрут — кирпич). Проверено: bash -n валиден для warp/off/uninstall, парсинг trace/public_ip ok, 73 passed. НЕ протестировано на живом боксе (нет доступного exit с SSH) — валидировать на свежем боксе. Старый бокс SERVER_IP всё ещё в кирпиче от прежнего full-tunnel: восстановить через provider console — warp-cli --accept-tos disconnect; systemctl stop warp-svc; systemctl disable warp-svc.

  • Починены 3 бага каскада RU↔US exit (выявлены полным тестом на реальном US-сервере): (1) каскад молча не поднимался — если на RU-узле отсутствовал /etc/wireguard/kaskad-entry.key, код брал stale entry_private_key из state (он разъезжается с реальным ключом entry, т.к. синкается только публичный), и RU-сторона туннеля предъявляла неверный ключ → exit отвергал handshake. Теперь local_entry_private_key() выбирает ключ, чей pubkey совпадает с entry_public_key, и самовосстанавливает файл; ensure_local_exit_tunnel_runtime возвращает явный статус route-tunnel-entry-key-mismatch вместо тихого мёртвого туннеля. (2) авто-очистка US при удалении выхода не срабатывала — условие запуска фонового cleanup проверяло ssh_auth_saved, которое добавляется только в sanitized_state (в сыром объекте его нет) → поток не стартовал. Теперь проверяется ssh_auth_type(). (3) deprovision не убирал metrics-SSH-ключ платформы из ~/.ssh/authorized_keys удаляемого US-сервера — теперь убирает. Проверено end-to-end: provision→handshake→delete→авто-очистка US (conf/iptables/metrics-key все убраны). 73 passed, деплой SERVER_IP.

  • Установка exit/RU по SSH теперь устойчива к шумным серверам: при добавлении US-выхода или RU-входа через SSH установщик повторяет подключение до 3 раз с паузой, если соединение временно отвалилось (timeout/reset/closed — типично для серверов под SSH-брутфорсом, где sshd под нагрузкой дропает часть коннектов). Ошибки авторизации/скрипта по-прежнему фейлят сразу, без лишних попыток. Также ConnectionAttempts в SSH поднят с 1 до 3. Это убирает ложное «SSH установка не удалась: port 22 timed out», когда сам сервер на самом деле доступен. 70 passed, деплой SERVER_IP.

  • Выборочное удаление/перенос (часть 2): при удалении RU входа или выхода модалка даёт выбор по каждому клиенту. Для RU входа: перенести на конкретный выбранный вход (с пометкой «нужна перенастройка») или удалить. Для выхода: авто/резерв, переназначить на другой выход или удалить. Есть пресет «Ко всем» для массового выбора одинакового действия+цели. Новые API POST /api/entry_servers/{id}/resolve_delete и POST /api/exit_servers/{id}/resolve_delete (атомарно: валидация всех решений до применения, snapshot, перенос/удаление, затем удаление сервера). При удалении клиентов с remote RU входа SSH-очистка их peer-ов вынесена в фоновый поток — недоступный по SSH вход не фризит API. Локальный ru-entry-1 по-прежнему не удаляется. Также добавлены чекбоксы массового выбора в списке клиентов и кнопка «Удалить выбранные» (с резервной копией перед удалением). Проверено: 68 passed, node --check ok, деплой SERVER_IP, prod API smoke (entry+exit resolve со cleanup) и UI-проверка в браузере (имя с кавычками/скобками, перенос на выбранный вход) — без ошибок.

  • Безопасность UI: jsString() теперь HTML-экранирует &, ", <, >, ' после JSON.stringify. Имена клиентов/серверов с кавычками, угловыми скобками или амперсандом вставляются в onclick="..." без разрыва атрибута и без поломки inline-скрипта (кнопки Имя/Удалить/Удалить аккаунт/удаление сервера). Браузер HTML-декодирует атрибут до парса JS, поэтому код остаётся валидным; & экранируется первым, чтобы не было двойного экранирования. Проверено node-симуляцией round-trip (кавычки, <script>, &, O'Brien, кириллица, null), 64 passed, node --check ok, деплой SERVER_IP.

  • Фикс «не работает удаление выхода»: deprovision_exit_over_ssh вызывался ВНУТРИ STORE.update с timeout=30, поэтому удаление недоступного по SSH exit держало state-lock до 30с и фризило всю API. Теперь remote SSH-cleanup вынесен в фоновый поток (best-effort, только если есть сохранённый SSH), а state-removal + local cleanup + reassign клиентов на auto делаются мгновенно. Exit без SSH (ssh_auth_saved пуст) удаляется сразу без сети. Prod-проверка: удаление offline exit Егор (SERVER_IP) заняло 0.43s, /api/status сразу отвечал 0.12s, клиент перешёл на auto. 64 passed.

  • Наглядное удаление серверов (часть 2 UI): кнопка Удалить у RU входа и exit открывает модалку, которая через /affected показывает затронутые аккаунты/устройства и даёт явный выбор. Для RU входа с клиентами: Перенести и удалить вход (клиенты на основной вход + пометка нужна перенастройка) или Удалить вход и N клиентов (bulk_delete клиентов + удаление входа); без клиентов — просто Удалить вход. Для exit: показывает сколько клиентов уйдёт на авто/резерв + Удалить выход. Локальный ru-entry-1 без кнопки удаления. Проверено в реальном браузере (preview): migrate/purge/exit-delete все отрабатывают, модалка наглядно показывает список. 64 passed, node --check ok, деплой SERVER_IP, prod содержит модалку.

  • Выборочное удаление/перенос (часть 1, backend + базовый UI): добавлены поля клиента needs_reconfigure + reconfigure_reason. При удалении RU входа с migrate_clients=true клиенты переносятся на основной вход и помечаются нужна перенастройка (endpoint меняется → нужен новый конфиг). Новые API: GET /api/entry_servers/{id}/affected и /api/exit_servers/{id}/affected (список затронутых аккаунтов/peers), POST /api/clients/bulk_delete (массовое удаление по списку id), DELETE /api/accounts/{id} (удалить аккаунт целиком), POST /api/accounts/{id}/reconfigured (снять пометку). Перед каждым destructive действием (delete entry/exit, bulk/account delete) пишется backup-снимок в data/backups/auto-*.json (0600, хранятся последние 30). Удаление exit НЕ ставит needs_reconfigure (Endpoint клиента = entry, конфиг не меняется). UI: badge нужна перенастройка в карточке клиента, кнопки Перенастроен ✓ и Удалить аккаунт. Локальный ru-entry-1 по-прежнему защищён от удаления. Тесты: 64 passed, node --check встроенного JS ok, деплой SERVER_IP. Осталось (часть 2): rich modal с выбором per-client (перенести на конкретный RU/exit / auto / удалить / пропустить) + bulk-select чекбоксы в списке + best-effort remote SSH cleanup при удалении RU.

  • Обновлены docs/notes.md и docs/claude-prompt.md для продолжения работы в Claude. В prompt добавлен свежий контекст по backup tab, migrate_clients=true, доменному переезду, WARP preflight, текущему offline exit Егор и следующей задаче: выборочная система удаления/переноса клиентов, badge требует перенастройки, account/peer/bulk delete, backup snapshot перед destructive action и best-effort remote cleanup.

  • Домен для RU-входа: новое поле endpoint_host на вход. Клиентские конфиги (WireGuard Endpoint, SOCKS5/Mieru host) используют домен, если задан, иначе IP (host). host (IP) остаётся для SSH/мониторинга/каскада entry→exit. Переезд на другой сервер = поменять A-запись домена, клиенты переподключаются без перенастройки (если ключи входа сохранены). UI: поле «Домен для клиентов» в форме добавления и в редакторе входа. Тесты: 56 passed, деплой SERVER_IP.

  • Критический фикс дашборда: cssString в DASHBOARD_HTML (не-raw Python-строка) недо-экранировала бэкслеши — браузер получал битый regex /\/g (вместо /\\/g), что давало SyntaxError и рушило ВЕСЬ inline-скрипт панели. Симптом: не работали кнопки (Имя/Копировать/Удалить, peer-действия, переключение вкладок), load() не запускался. Удвоены бэкслеши до /\\\\/g, '\\\\\\\\'. Проверено: node --check ok локально и на prod, скрипт исполняется, делегированные data-peer-action кнопки и showView снова работают. 54 passed, деплой SERVER_IP.

  • Исправлена кнопка Имя в таблице серверов: вместо нестабильного browser prompt кнопка теперь раскрывает редактор Данные, фокусирует поле Название и выделяет текст. Сохранение идёт через существующий /settings API. Prod smoke: прямой rename API Екатерина_германия -> *_smoke -> Екатерина_германия ok, dashboard содержит новый editServerName.

  • Исправлено зависание WARP-установки: WARP больше не выполняется внутри STORE.update и не блокирует весь state/API. Клик запускает background job, строка exit получает статусы installing / disconnecting / removing / error, UI сразу отпускает кнопку и продолжает обновляться. SSH усилен ConnectionAttempts=1, ServerAliveInterval=15, ServerAliveCountMax=2; apt/curl внутри install ограничены timeout. Если сервис перезапущен во время WARP job, stale pending статус через 15 минут превращается в error. Prod diagnosis: exit SERVER_IP недоступен с RU (ping 0, TCP 22 timeout), поэтому WARP не ставится до исправления SSH/доступности exit.

  • Исправлено зависание/ощущение зависания верхних кнопок Проверить выходы, Переключить выход, Сверить маршруты: API больше не возвращает полный /api/status после действия, а отдаёт компактный {ok:true}; UI затем делает один обычный refresh. Кнопки теперь передают this в progress flow, получают busy-состояние и видимый статус. Prod smoke: ответы 21-45 bytes, check_all ~2 sec на текущем offline exit.

  • Добавлено управление NaiveProxy в Настройки WG: выбор конкретного сервера (local, RU entry, US exit), домен, email, user/password, порт, status, client URI, установка и удаление. Backend API: GET /api/naiveproxy, POST /api/naiveproxy. Установка собирает Caddy через xcaddy с github.com/klzgrad/forwardproxy@naive, включает hide_ip, hide_via, probe_resistance, отдаёт нейтральный HTML-сайт обычному браузеру. Защита: без домена установка запрещена; если порт занят чужим listener, установка блокируется; секрет хранится encrypted и не попадает в /api/status. Важно: NaiveProxy — TCP HTTPS proxy, не нативная замена WireGuard UDP каскада RU↔US.

  • Добавлена отдельная кнопка AWG 2.0 у RU входов. Она ставит AmneziaWG 2.0 на отдельный интерфейс kaskad-awg2 с отдельным портом и сетью 10.79.0.0/24, не перезаписывает Legacy kaskad-awg. Клиенты AmneziaWG 2.0 после установки получают endpoint/key/peer sync именно через kaskad-awg2. Также исправлена таблица серверов: действия переносятся внутри строки и не вылезают за правый край.

  • Добавлено полноценное редактирование данных RU/US серверов. В таблице серверов появилась кнопка Данные: можно менять название, host/IP, страну, порт, public key, priority/вес, режим RU входа (WireGuard / AmneziaWG) и флаг включения. Для US/foreign выходов снятие Сервер включен выключает exit, чистит ссылки маршрутов и переводит клиентов на авто/резерв. SSH логин/порт/пароль/private key остаются отдельной кнопкой SSH.

  • Установлен и проверен AmneziaWG Legacy/v1 на главном RU SERVER_IP: awg-quick@kaskad-awg active, awg слушает отдельный порт 51821, UI показывает зелёный AWG Legacy active :51821.

  • Исправлена логика AWG-клиентов на RU входах с отдельным kaskad-awg: AmneziaWG Legacy теперь считается поддержанным, если у entry есть awg_installed=true; AmneziaWG 2.0 остаётся заблокированным на v1 tools и требует v2-capable installer.

  • Исправлена генерация Legacy-конфигов: из AmneziaWG Legacy убраны v2-only поля S3/S4/I1.., даже если они остались в старом JSON state. Это важно для телефонов и старых AWG клиентов.

  • Исправлена runtime-синхронизация удалённых RU входов: создание/выключение/удаление клиента на remote RU теперь добавляет/удаляет peer через сохранённый SSH на нужном интерфейсе (wg kaskad-entry или awg kaskad-awg), вместо старого skip-remote-entry.

  • Исправлен WARP timeout handler: bytes-вывод subprocess.TimeoutExpired больше не вызывает TypeError/500, UI получает нормальную ошибку 504 WARP timeout.

  • Расширены тесты AWG: Legacy v1-safe config, reject для 2.0 на v1 install, remote AWG peer sync/remove. Локально: embedded JS node --check ok, py_compile ok, 44 passed.

  • Исправлена смена имени RU/US серверов в UI. Кнопки Имя теперь работают через общий progress flow, проверяют HTTP status, показывают ошибку вместо тихого load(). Prod smoke SERVER_IP: entry-e22af565 переименован в ru_rename_smoke и возвращен обратно в ru; dashboard отдаёт новый JS.

  • Добавлен видимый прогресс долгих действий в web UI: fixed progress-toast с процентами, shimmer-индикатором и busy-состоянием кнопки. Обёрнуты основные операции добавления/удаления и настройки: RU/US server add/provision, SSH save, WARP install/disable/remove, create client, add/delete peer, delete client, delete RU/exit, check entry/exit/all, failover/reconcile, enable/disable client/account/exit, смена exit. Ошибки теперь остаются видимыми в progress-toast и actionStatus, без ощущения зависания.

2026-06-07

  • Исправлен UX добавления новых RU/US серверов: при выборе Установить по SSH теперь стабильно появляются поля SSH логин, SSH порт, SSH пароль, SSH private key. Inline onchange заменён на delegated change handler + syncSshFields() после загрузки/refresh, чтобы браузерное восстановление select не оставляло SSH-блок скрытым. На prod SERVER_IP проверено: service active, source содержит data-ssh-toggle, local 43 passed, prod smoke всех основных кнопок ok.
  • Исправлены peer-кнопки на вкладке Клиенты внутри раскрытой карточки пользователя: Имя, Конфиг, Копировать, QR, Скачать, Удалить теперь работают прямо с этой страницы, без перехода во вкладку Конфиги. Inline onclick заменён на делегированный обработчик clientsTable[data-peer-action], чтобы действия не ломались из-за вложенных карточек/form/row-click. Prod SERVER_IP: py_compile, node --check, 43 passed, service active, API smoke peer_button_api_ok.
  • Исправлены действия peer/client в новом дизайне: Имя, Копировать, Удалить и соседние кнопки теперь не конфликтуют с form submit/row click (type="button" + stopPropagation). Копирование конфигов работает на обычном HTTP через fallback textarea, если navigator.clipboard недоступен. Rename/delete теперь проверяют HTTP-ошибки API и показывают понятный alert. Проверено локально (py_compile, node --check, 43 passed) и на prod SERVER_IP: временный клиент создан, переименован, config/download/QR отданы, удалён, мусора в state не осталось.
  • Telegram reachability на prod SERVER_IP починена: системный DNS отдавал для api.telegram.org только IPv6 (RU DNS-фильтр), а код форсит IPv4 → getUpdates/sendMessage таймаутили. Прямой IPv4 149.154.167.220:443 доступен. Добавлен пин 149.154.167.220 api.telegram.org в /etc/hosts на сервере (не в repo). telegram_api теперь ловит HTTPError и возвращает JSON-тело Telegram (401/409/429 как {ok:false,...}) вместо исключения. Бот всё ещё не отвечает, пока в Настройки WG → Telegram не задан bot_token и не включён флаг.
  • Дашборд доведён до макета (вторая итерация рестайла): KPI-карточки получили цветные иконки-чипы (server/exit/globe/users/file, тона slate/blue/green/violet/amber); вкладка Обзор — flow-топология Клиенты → RU входы → US выходы (CPU-метры) → Интернет вместо текстовых hop; форма «Настройки WireGuard» переведена в 2-колоночную сетку с лейблами и mono-инпутами. Live-данные/действия не тронуты. 43 passed, деплой SERVER_IP, проверено визуально (preview, все вкладки).
  • Веб-дашборд переведён на Apple-grade дизайн-систему (из Claude Design handoff Kaskad Platform.html):
    • слой design-refresh поверх DASHBOARD_HTML (токены/тени/радиусы/SF-шрифт, ничего из live-логики не тронуто);
    • topbar с brand-mark (зелёный градиент + молния), KPI-карточки (5, крупные числа, тени);
    • сегментные табы (контейнер + активная белая вкладка);
    • кнопки в стиле дизайна (green primary / blue / danger / secondary / ghost, размеры sm/xs);
    • вкладка Клиенты: раскрываемые карточки (теги, route, traffic-pill, amber expiry, toggle, detail-блоки);
    • вкладка Серверы: таблица в стиле .tbl (uppercase-заголовки, role-pills, hover, WARP-pill);
    • остальные вкладки наследуют новую систему. Деплой на SERVER_IP, проверено визуально (preview).
  • Telegram бот получил админ-управление оплатами (порт идей из xui-subscription-monitor, на JSON-state, без цен):
    • нумерованные заявки Заявка на оплату #N со статусами pending/processing/approved/rejected в state.payments + payment_seq;
    • защита от двойного подтверждения через claim (pending -> processing);
    • admin reply-меню: Отчёт за месяц, Кто оплатил, Не оплатили, Ожидают оплаты, Рассылка, Отмена;
    • отчёты считают оплаты/телефоны/должников (≤7 дн. без оплаты в месяце) без денежных сумм;
    • рассылка всем привязанным TG-клиентам;
    • богатая карточка подписки (аккаунт, телефон, примечание, протокол, выход, статус, окончание, осталось N дн.);
    • тесты 43 passed; деплой на SERVER_IP, миграция state и health проверены.
  • Повторно проверен новый prod SERVER_IP: локально 41 passed, service/WG active, key sync ok, API audit ok, final cleanup ok; добавлен Claude handoff prompt.
  • Исправлена синхронизация public key локального RU входа: /api/status и клиентские конфиги теперь берут live key из wg show kaskad-entry public-key, а для AWG - из awg show kaskad-awg public-key.
  • Добавлена валидация live public key: случайный stdout диагностики не может попасть в state как ключ.
  • Исправлен SOCKS5 runtime: proxy больше не биндуется к 10.77.0.1, Python runtime сам ограничивает активные соединения, stale systemd/config файлы удаляются после удаления клиента.
  • Production audit на SERVER_IP: проверены RU add/delete, exit enable/check/delete, WARP error handling без SSH, WireGuard client config/QR/enable/delete, SOCKS5 config/QR/delete, AWG reject на обычном WG входе, AWG Legacy/2.0 config на AWG entry.
  • Проверен живой WireGuard runtime: временный peer добавился в kaskad-entry и удалился обратно.
  • Router SSH audit временно заблокирован: ROUTER_IP не принимает переданные SSH логины/пароли. Нужен актуальный SSH доступ или web/API-доступ для проверки handshake на роутере.
  • Перенесен default RU entry на новый главный RU server SERVER_IP.
  • Новый деплой поднят как чистая платформа: 1 RU вход, без клиентов и exit-серверов.
  • Проверен вход панели admin/admin, health API и wg-quick@kaskad-entry.
  • Зафиксирован блокер Telegram: новый RU server не достучался до api.telegram.org, а SERVER_IP доступен по UDP WireGuard, но недоступен по SSH и не дает WG handshake для нового RU.
  • Добавлен блок управления нейтральным сайтом маскировки на порту 80.
  • Добавлена защита установки сайта при занятом 80 порту чужим процессом или чужим nginx default-сайтом.
  • Добавлено открытие клиентского конфига в модальном окне с QR, копированием и информацией по клиенту.
  • Усилена безопасность панели: rate-limit логина, security headers, Secure cookie при HTTPS.
  • Добавлен флаг auth.default_credentials в /api/status и предупреждение в UI при активном admin/admin.
  • Ограничена длина логина/пароля, новый пароль минимум 8 символов.
  • JSON state и secret key хранятся с правами 0600, data directory 0700.
  • Исправлено JS-экранирование имён в действиях удаления клиента/exit.
  • Добавлена SSH-установка AmneziaWG вход: установка amneziawg, генерация ключей через awg, запуск awg-quick@kaskad-entry.
  • Расширены автотесты безопасности, auth и site-mask.