Замер мерил не выход, а зеркало: 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>
72 KiB
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 193–259 КБ/с против cachefly 8.8–11 МБ/с. Теперь быстрые зеркала первыми (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, TUNaivpn0, ip rulesfrom 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_compileok; тесты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/healthok,/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_compileok, embedded JSnode --checkok,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: alias10.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, JSnode --checkok, задеплоено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), addr10.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) backupWireguard4(sber_1) самостоятельно держал каскад — 0% loss, egressSERVER_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→testip=SERVER_IP, 204→SGA_3ip=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, deploySERVER_IPok,/api/healthok, повторныйsync_remote_entry_cascadeиз задеплоенного кода → все 3 exit отдают 3 backup-маршрута (идемпотентно). - СЛЕДУЮЩЕЕ (P2): dual-entry peer model — синхронизировать peer клиента на ОБА входа + 2 конфига Keenetic (primary
pcastrosole.ru/ backupglobalsecure.ru) + UI «Keenetic failover». Известная мелочь P1: на sber_1 ip rulefrom 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. Следующий чат должен продолжить с tcpdumpkaskad-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/24WG /10.88AWG /10.89AWG2), чтобы у 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), AllowedIPs0.0.0.0/0, Table=off),ip route default dev kx table <N>, FORWARD-правила (-i kaskad-entry -o kx, returnRELATED,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-й address10.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) без конфликта возврата.
- на резервном RU (sber_1) для каждого exit: WG-интерфейс
-
Диагностика (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 только boolwarp_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/status171КБ→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/configok. - 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:ru→category-ru. Домены резолвятся в IP (capGEOSITE_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: один ipsetkaskad_ru(bulkipset 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 = Cloudflare104.28.219.137, egress бокса прямой, SSH жив (brick-proof подтверждён). Return-rule добавлен в live-confs AMS-WARP/US_фин. -
Починены 2 бага каскада, из-за которых клиент роутера (
test10.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, а confkaskad-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-awgconfs в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): exitAMS-WARPдобавлен через API панели, кнопка WARP отработала новый wgcf-скрипт. Результат: panel WARPstatus=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 warpfull-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, код брал staleentry_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 --checkok, деплой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 --checkok, деплой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 --checkok, деплой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на вход. Клиентские конфиги (WireGuardEndpoint, 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 --checkok локально и на prod, скрипт исполняется, делегированныеdata-peer-actionкнопки иshowViewснова работают.54 passed, деплойSERVER_IP. -
Исправлена кнопка
Имяв таблице серверов: вместо нестабильного browserpromptкнопка теперь раскрывает редакторДанные, фокусирует полеНазваниеи выделяет текст. Сохранение идёт через существующий/settingsAPI. 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: exitSERVER_IPнедоступен с RU (ping 0, TCP22timeout), поэтому 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, не перезаписывает Legacykaskad-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-awgactive,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 --checkok,py_compileok,44 passed. -
Исправлена смена имени RU/US серверов в UI. Кнопки
Имятеперь работают через общий progress flow, проверяют HTTP status, показывают ошибку вместо тихогоload(). Prod smokeSERVER_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. Inlineonchangeзаменён на delegatedchangehandler +syncSshFields()после загрузки/refresh, чтобы браузерное восстановление select не оставляло SSH-блок скрытым. На prodSERVER_IPпроверено: service active, source содержитdata-ssh-toggle, local43 passed, prod smoke всех основных кнопок ok. - Исправлены peer-кнопки на вкладке
Клиентывнутри раскрытой карточки пользователя:Имя,Конфиг,Копировать,QR,Скачать,Удалитьтеперь работают прямо с этой страницы, без перехода во вкладкуКонфиги. Inlineonclickзаменён на делегированный обработчикclientsTable[data-peer-action], чтобы действия не ломались из-за вложенных карточек/form/row-click. ProdSERVER_IP:py_compile,node --check,43 passed, service active, API smokepeer_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) и на prodSERVER_IP: временный клиент создан, переименован, config/download/QR отданы, удалён, мусора в state не осталось. - Telegram reachability на prod
SERVER_IPпочинена: системный DNS отдавал дляapi.telegram.orgтолько IPv6 (RU DNS-фильтр), а код форсит IPv4 → getUpdates/sendMessage таймаутили. Прямой IPv4149.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).
- слой design-refresh поверх
- 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,
Securecookie при HTTPS. - Добавлен флаг
auth.default_credentialsв/api/statusи предупреждение в UI при активномadmin/admin. - Ограничена длина логина/пароля, новый пароль минимум 8 символов.
- JSON state и secret key хранятся с правами
0600, data directory0700. - Исправлено JS-экранирование имён в действиях удаления клиента/exit.
- Добавлена SSH-установка
AmneziaWG вход: установкаamneziawg, генерация ключей черезawg, запускawg-quick@kaskad-entry. - Расширены автотесты безопасности, auth и site-mask.