Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9F4tUKVN3qEnK8s6FYhs4
This commit is contained in:
StressOzzandClaude Opus 5.5 committed 2026-10-20 18:40:03 +00:00
1 parent b8f00965ed
commit c65f8c82ad
2 files changed
+315 -74

No files matched your search

+31
View File
@@ -1,3 +1,34 @@
## 3.93 — 20.10.2026
### 🎯 Автоподбор стратегии
- ➕ Режим «Только YouTube»: подбирается стратегия Yv, основная остаётся как есть
- ➕ Переключатель «Подбирать и стратегию для YouTube»: после основной стратегии подбирается Yv на адресах YouTube
- ⚡ Новая стратегия YouTube ставится, только если открывает больше адресов, чем текущая
- ⚡ Если YouTube выключен в стратегии или включено «Не изменять стратегию», подбор для YouTube пропускается
- ➕ Результат подбора для YouTube показывается отдельной строкой в «Последний запуск»
- ⚡ Режим автоподбора можно выбрать, даже когда расписание выключено
### 📱 Удалённый доступ
- ⚡ Раздел Tailscale переименован в «Удалённый доступ»
- ➕ Кнопка «У меня всё работает»: одобрение роутера можно отметить вручную, если сервер не сообщает о нём
- ➕ Строка «Одобрение — отмечено вручную» с кнопкой «Снять отметку»; при смене сервера отметка сбрасывается сама
- 🐞 Доступ к домашней сети распознаётся, даже если сервер одобрил более широкую подсеть
- ⚡ Для Android — ссылка на последнюю версию Tailscale с GitHub и TailSocks, ссылка на Google Play убрана
### 🌐 Steer
- 🐞 Проверка маршрута домена работает и при правилах для отдельных устройств
### 🎨 Web UI
- ➕ Кнопка «Готово» в настройке меню, кнопки разложены сеткой
- ⚡ «Скрыть неустановленные» скрывает и Hosts с блокировкой рекламы, если они не включены
- ⚡ Баннер обновления аккуратно переносится на телефоне
### 🧩 Прочее
- ⚡ Карточки системной информации раскладываются по колонкам ровнее, без пустот
- ⚡ Подписи и значения в системной информации не наезжают друг на друга
- ⚡ В «Доступности сайтов» сначала показываются недоступные
- ➕ В «Спасибо» добавлены openwrt-tailscale, luci-app-tailscale-community, NaïveProxy и tg-ws-proxy, у routerich — сервер удалённого доступа
## 3.90 — 20.10.2026 ## 3.90 — 20.10.2026
### 🛡 Zapret ### 🛡 Zapret
+284 -74
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
read -r _ _ ZM_NEW_VER <<'ZM_VERSION_EOF' read -r _ _ ZM_NEW_VER <<'ZM_VERSION_EOF'
# Version: 3.90 # Version: 3.93
ZM_VERSION_EOF ZM_VERSION_EOF
set -e set -e
@@ -3642,7 +3642,7 @@ _pg_st_dir() {
} }
_pg_trace() { _pg_trace() {
case "$1" in case "$1" in
zapret) echo "pkg:luci-i18n-zapret-ru pkg:luci-app-zapret pkg:zapret /opt/zapret /etc/config/zapret /etc/config/zapret-opkg /etc/init.d/zapret /etc/rc.d/*[0-9]zapret /etc/firewall.zapret /etc/hotplug.d/*/*zapret /etc/config/zapret.apk-new /tmp/zapret $(_zm_luci_files zapret) nft:zapret cron:zm-autobest $ZO_RKN_FLAG $ZO_USER_FLAG $ZA_MODE_FILE $ZA_LAST $ZA_TS $ZA_TIME_FILE $TEST_BACKUP $ZC_FILE $ZC_BAK $ZC_AFTER $YV_OFF_FLAG $DV_OFF_FLAG $TEST_DIR $TEST_CUSTOM_FILE" ;; zapret) echo "pkg:luci-i18n-zapret-ru pkg:luci-app-zapret pkg:zapret /opt/zapret /etc/config/zapret /etc/config/zapret-opkg /etc/init.d/zapret /etc/rc.d/*[0-9]zapret /etc/firewall.zapret /etc/hotplug.d/*/*zapret /etc/config/zapret.apk-new /tmp/zapret $(_zm_luci_files zapret) nft:zapret cron:zm-autobest $ZO_RKN_FLAG $ZO_USER_FLAG $ZA_MODE_FILE $ZA_LAST $ZA_YT_FILE $ZA_LAST_YT $ZA_TS $ZA_TIME_FILE $TEST_BACKUP $ZC_FILE $ZC_BAK $ZC_AFTER $YV_OFF_FLAG $DV_OFF_FLAG $TEST_DIR $TEST_CUSTOM_FILE" ;;
zapret2) echo "pkg:luci-i18n-zapret2-ru pkg:luci-app-zapret2 pkg:zapret2 /opt/zapret2 /etc/config/zapret2 /etc/config/zapret2-opkg /etc/init.d/zapret2 /etc/rc.d/*zapret2 /etc/hotplug.d/*/*zapret2 /etc/config/zapret2.apk-new /tmp/zapret2* $(_zm_luci_files zapret2) nft:zapret2 cronrx:zapret2" ;; zapret2) echo "pkg:luci-i18n-zapret2-ru pkg:luci-app-zapret2 pkg:zapret2 /opt/zapret2 /etc/config/zapret2 /etc/config/zapret2-opkg /etc/init.d/zapret2 /etc/rc.d/*zapret2 /etc/hotplug.d/*/*zapret2 /etc/config/zapret2.apk-new /tmp/zapret2* $(_zm_luci_files zapret2) nft:zapret2 cronrx:zapret2" ;;
bytetube) bytetube)
echo "pkg:byedpi /usr/bin/ciadpi /etc/init.d/byedpi /etc/rc.d/*byedpi /etc/config/byedpi /etc/config/byedpi-opkg /usr/bin/bytetube /etc/config/bytetube /etc/init.d/bytetube /etc/rc.d/*bytetube /usr/libexec/bytetube /usr/share/bytetube /etc/bytetube /etc/hotplug.d/firewall/90-bytetube /usr/share/nftables.d/chain-pre/forward/50-bytetube.nft /var/etc/bytetube /var/run/bytetube.started /tmp/bytetube-test /lib/upgrade/keep.d/bytetube /tmp/dhcp.ytb.bak dnsconf:bytetube.conf cronrx:bytetube" echo "pkg:byedpi /usr/bin/ciadpi /etc/init.d/byedpi /etc/rc.d/*byedpi /etc/config/byedpi /etc/config/byedpi-opkg /usr/bin/bytetube /etc/config/bytetube /etc/init.d/bytetube /etc/rc.d/*bytetube /usr/libexec/bytetube /usr/share/bytetube /etc/bytetube /etc/hotplug.d/firewall/90-bytetube /usr/share/nftables.d/chain-pre/forward/50-bytetube.nft /var/etc/bytetube /var/run/bytetube.started /tmp/bytetube-test /lib/upgrade/keep.d/bytetube /tmp/dhcp.ytb.bak dnsconf:bytetube.conf cronrx:bytetube"
@@ -5342,7 +5342,7 @@ test_action() {
job_start strategy_test do_autobest job_start strategy_test do_autobest
;; ;;
auto_tick) test_auto_tick ;; auto_tick) test_auto_tick ;;
auto_clear) rm -f "$ZA_LAST"; test_status ;; auto_clear) rm -f "$ZA_LAST" "$ZA_LAST_YT"; test_status ;;
stop) stop)
_test_running || { echo '{"error":"тест не запущен"}'; return 1; } _test_running || { echo '{"error":"тест не запущен"}'; return 1; }
mkdir -p "$TEST_DIR" mkdir -p "$TEST_DIR"
@@ -5582,7 +5582,10 @@ ZA_LAST="$ZM_STATE_DIR/autobest.last"
ZA_RUN="$TEST_DIR/autobest" ZA_RUN="$TEST_DIR/autobest"
ZA_TS="$ZM_STATE_DIR/autobest.ts" ZA_TS="$ZM_STATE_DIR/autobest.ts"
_za_mode() { case "$(cat "$ZA_MODE_FILE" 2>/dev/null)" in v) echo v ;; flowseal) echo flowseal ;; *) echo v_flowseal ;; esac; } ZA_YT_FILE="$ZM_STATE_DIR/autobest.yt"
ZA_LAST_YT="$ZM_STATE_DIR/autobest.last_yt"
_za_mode() { case "$(cat "$ZA_MODE_FILE" 2>/dev/null)" in v) echo v ;; flowseal) echo flowseal ;; youtube) echo youtube ;; *) echo v_flowseal ;; esac; }
_za_yt() { [ -f "$ZA_YT_FILE" ]; }
ZA_TIME_FILE="$ZM_STATE_DIR/autobest.time" ZA_TIME_FILE="$ZM_STATE_DIR/autobest.time"
_za_time() { grep -F "$ZA_TAG" "$CRON_FILE" 2>/dev/null | head -n 1 | awk '{ printf "%02d:%02d\n", $2, $1 }'; } _za_time() { grep -F "$ZA_TAG" "$CRON_FILE" 2>/dev/null | head -n 1 | awk '{ printf "%02d:%02d\n", $2, $1 }'; }
_za_running() { [ -f "$ZA_RUN" ] && _test_running; } _za_running() { [ -f "$ZA_RUN" ] && _test_running; }
@@ -5634,28 +5637,93 @@ _za_extras_restore() {
_za_last_write() { _za_last_write() {
mkdir -p "$ZM_STATE_DIR" mkdir -p "$ZM_STATE_DIR"
printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$(date +%s)" "$1" "$2" "$3" "$4" "$5" "$6" "$7" > "$ZA_LAST" printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$(date +%s)" "$1" "$2" "$3" "$4" "$5" "$6" "$7" > "${ZA_LAST_TO:-$ZA_LAST}"
} }
do_autobest() { do_autobest() {
local mode res best bname bok btot cok ctot cname urls applied=0 r local mode rc=0
mode="$(_za_mode)" mode="$(_za_mode)"
mkdir -p "$TEST_DIR"; : > "$ZA_RUN" mkdir -p "$TEST_DIR"; : > "$ZA_RUN"
echo "==> Автоподбор стратегии: $(case "$mode" in v) echo v1–v10 ;; flowseal) echo Flowseal ;; *) echo 'v + Flowseal' ;; esac)" echo "==> Автоподбор стратегии: $(case "$mode" in v) echo v1–v10 ;; flowseal) echo Flowseal ;; youtube) echo 'только YouTube (Yv)' ;; *) echo 'v + Flowseal' ;; esac)$([ "$mode" != youtube ] && _za_yt && echo ', затем YouTube (Yv)')"
[ -f "$CONF" ] || { echo "ОШИБКА: Zapret не установлен"; rm -f "$ZA_RUN"; return 1; } [ -f "$CONF" ] || { echo "ОШИБКА: Zapret не установлен"; rm -f "$ZA_RUN"; return 1; }
_test_unstale _test_unstale
rm -f "$ZA_LAST_YT"
if _nochange_strategy; then if _nochange_strategy; then
echo "==> Включено «Не изменять стратегию» (#nochange) — автоподбор ничего не меняет" echo "==> Включено «Не изменять стратегию» (#nochange) — автоподбор ничего не меняет"
_za_last_write "$mode" "" 0 0 0 "" nochange; rm -f "$ZA_RUN"; return 0 _za_last_write "$mode" "" 0 0 0 "" nochange; rm -f "$ZA_RUN"; return 0
fi fi
ZT_STOPPED=""
if [ "$mode" = youtube ]; then
_za_youtube; rc=$?
else
_za_main "$mode"; rc=$?
if [ "$rc" = 0 ] && [ -z "$ZT_STOPPED" ] && _za_yt; then
ZA_LAST_TO="$ZA_LAST_YT"; _za_youtube; ZA_LAST_TO=""
fi
fi
rm -f "$ZA_RUN"
return "$rc"
}
_za_youtube() {
local res best yname yok ytot cur cok ctot r applied=0
echo "==> YouTube: подбираем стратегию Yv"
if [ -f "$YV_OFF_FLAG" ]; then
echo " YouTube выключен в стратегии — его настройку не трогаем"
_za_last_write youtube "" 0 0 0 "" yoff; return 0
fi
if _nochange_strategy; then
echo "==> Включено «Не изменять стратегию» (#nochange) — YouTube не трогаем"
_za_last_write youtube "" 0 0 0 "" nochange; return 0
fi
cur="$(grep -oE '^#[[:space:]]*Yv[0-9]+$' "$CONF" | head -n1 | sed 's/^#[[:space:]]*//')"
do_test_run youtube || { _za_last_write youtube "" 0 0 0 "$cur" error; return 1; }
res="$(_test_results_file youtube)"
best="$(grep -v '^Контрольный тест' "$res" 2>/dev/null | head -n 1)"
if [ -z "$best" ] || [ -n "$ZT_STOPPED" ]; then
echo "==> YouTube: лучшую стратегию определить не удалось — оставляем текущую"
_za_last_write youtube "" 0 0 0 "$cur" none; return 0
fi
yname="$(printf '%s' "$best" | sed 's/[[:space:]]*→.*$//')"
yok="$(printf '%s' "$best" | sed -n 's/.*→[[:space:]]*\([0-9]*\)\/\([0-9]*\).*/\1/p')"
ytot="$(printf '%s' "$best" | sed -n 's/.*→[[:space:]]*\([0-9]*\)\/\([0-9]*\).*/\2/p')"
echo "==> Проверяем текущую настройку YouTube (${cur:-из основной стратегии}) на тех же адресах"
r="$(_test_check_all_urls "$(_test_yt_urls)" "$TEST_DIR/log_cur_yt.txt")"
cok="${r% *}"; ctot="${r#* }"
echo " текущая: ${cok}/${ctot}, лучшая из теста: $yname — ${yok}/${ytot}"
if [ "$yname" != "$cur" ] && [ "${yok:-0}" -gt "${cok:-0}" ] 2>/dev/null; then
if _nochange_strategy; then
echo "==> Пока шёл тест, включили «Не изменять стратегию» — ничего не меняем"
_za_last_write youtube "$yname" "$yok" "$ytot" "$cok" "$cur" nochange; return 0
fi
echo "==> Применяем $yname для YouTube"
_zc_lift
r="$(strategy_set_youtube "$yname" 2>/dev/null)"
_zc_drop
case "$r" in *'"ok":true'*) applied=1; echo "==> Готово: стратегия YouTube $yname применена" ;; *) echo "!! Не удалось применить $yname — YouTube оставлен как был" ;; esac
if [ "$applied" = 1 ] && _nfq_opt_body | _ts_has; then
date +%s > "$ZA_TS"
echo " ! в стратегии есть метка ts — на компьютерах с Windows нужно один раз выполнить:"
echo " netsh int tcp set global timestamps=enabled (в терминале от имени администратора)"
fi
[ "$applied" = 1 ] && logger -t zapret-manager "Автоподбор: для YouTube применена $yname ($yok/$ytot, было $cok/$ctot)"
else
echo "==> Текущая настройка YouTube не хуже — оставляем её"
fi
_za_last_write youtube "$yname" "$yok" "$ytot" "$cok" "$cur" "$([ "$applied" = 1 ] && echo applied || echo kept)"
return 0
}
_za_main() {
local mode="$1" res best bname bok btot cok ctot cname urls applied=0 r
cname="$(_nfq_names | sed 's/^#[[:space:]]*//' | grep -vE '^(udp443|Yv[0-9]+|Dv[0-9]+|Gv[0-9]+.*)$' | head -n 1)" cname="$(_nfq_names | sed 's/^#[[:space:]]*//' | grep -vE '^(udp443|Yv[0-9]+|Dv[0-9]+|Gv[0-9]+.*)$' | head -n 1)"
[ -n "$cname" ] || cname="$(grep -m1 '^# ZMFS:' "$CONF" | sed 's/^# ZMFS://')" [ -n "$cname" ] || cname="$(grep -m1 '^# ZMFS:' "$CONF" | sed 's/^# ZMFS://')"
do_test_run "$mode" || { _za_last_write "$mode" "" 0 0 0 "" error; rm -f "$ZA_RUN"; return 1; } do_test_run "$mode" || { _za_last_write "$mode" "" 0 0 0 "" error; return 1; }
res="$(_test_results_file "$mode")" res="$(_test_results_file "$mode")"
best="$(grep -v '^Контрольный тест' "$res" 2>/dev/null | head -n 1)" best="$(grep -v '^Контрольный тест' "$res" 2>/dev/null | head -n 1)"
if [ -z "$best" ] || [ -n "$ZT_STOPPED" ]; then if [ -z "$best" ] || [ -n "$ZT_STOPPED" ]; then
echo "==> Лучшую стратегию определить не удалось — оставляем текущую" echo "==> Лучшую стратегию определить не удалось — оставляем текущую"
_za_last_write "$mode" "" 0 0 0 "" none; rm -f "$ZA_RUN"; return 0 _za_last_write "$mode" "" 0 0 0 "" none; return 0
fi fi
bname="$(printf '%s' "$best" | sed 's/[[:space:]]*→.*$//')" bname="$(printf '%s' "$best" | sed 's/[[:space:]]*→.*$//')"
bok="$(printf '%s' "$best" | sed -n 's/.*→[[:space:]]*\([0-9]*\)\/\([0-9]*\).*/\1/p')" bok="$(printf '%s' "$best" | sed -n 's/.*→[[:space:]]*\([0-9]*\)\/\([0-9]*\).*/\1/p')"
@@ -5668,7 +5736,7 @@ do_autobest() {
if [ "${bok:-0}" -gt "${cok:-0}" ] 2>/dev/null; then if [ "${bok:-0}" -gt "${cok:-0}" ] 2>/dev/null; then
if _nochange_strategy; then if _nochange_strategy; then
echo "==> Пока шёл тест, включили «Не изменять стратегию» — ничего не меняем" echo "==> Пока шёл тест, включили «Не изменять стратегию» — ничего не меняем"
_za_last_write "$mode" "$bname" "$bok" "$btot" "$cok" "$cname" nochange; rm -f "$ZA_RUN"; return 0 _za_last_write "$mode" "$bname" "$bok" "$btot" "$cok" "$cname" nochange; return 0
fi fi
echo "==> Применяем $bname" echo "==> Применяем $bname"
_zc_has && echo " · свой блок #CustomStart … #CustomEnd сохраняем — он останется в самом верху стратегии" _zc_has && echo " · свой блок #CustomStart … #CustomEnd сохраняем — он останется в самом верху стратегии"
@@ -5692,13 +5760,13 @@ do_autobest() {
echo "==> Текущая стратегия не хуже — оставляем её" echo "==> Текущая стратегия не хуже — оставляем её"
fi fi
_za_last_write "$mode" "$bname" "$bok" "$btot" "$cok" "$cname" "$([ "$applied" = 1 ] && echo applied || echo kept)" _za_last_write "$mode" "$bname" "$bok" "$btot" "$cok" "$cname" "$([ "$applied" = 1 ] && echo applied || echo kept)"
rm -f "$ZA_RUN"
return 0 return 0
} }
test_auto_set() { test_auto_set() {
local when="${1%%|*}" mode="${1#*|}" h m local when="${1%%|*}" mode="${1#*|}" yt="" h m
case "$mode" in v|flowseal|v_flowseal) ;; *) mode="$(_za_mode)" ;; esac case "$mode" in *'|'*) yt="${mode#*|}"; mode="${mode%%|*}" ;; esac
case "$mode" in v|flowseal|v_flowseal|youtube) ;; *) mode="$(_za_mode)" ;; esac
case "$when" in case "$when" in
off) ;; off) ;;
[0-9]:[0-5][0-9]|[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9]) [0-9]:[0-5][0-9]|[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9])
@@ -5707,6 +5775,7 @@ test_auto_set() {
*) echo '{"error":"неверное время"}'; return 1 ;; *) echo '{"error":"неверное время"}'; return 1 ;;
esac esac
mkdir -p "$ZM_STATE_DIR"; echo "$mode" > "$ZA_MODE_FILE" mkdir -p "$ZM_STATE_DIR"; echo "$mode" > "$ZA_MODE_FILE"
case "$yt" in on) : > "$ZA_YT_FILE" ;; off) rm -f "$ZA_YT_FILE" ;; esac
mkdir -p "$(dirname "$CRON_FILE")"; touch "$CRON_FILE" mkdir -p "$(dirname "$CRON_FILE")"; touch "$CRON_FILE"
sed -i "\\|$ZA_TAG|d" "$CRON_FILE" sed -i "\\|$ZA_TAG|d" "$CRON_FILE"
if [ "$when" != off ]; then if [ "$when" != off ]; then
@@ -5727,23 +5796,30 @@ test_auto_tick() {
job_start strategy_test do_autobest >/dev/null job_start strategy_test do_autobest >/dev/null
} }
_za_last_json() {
local last at m b bo bt co cn res
last="$(cat "$1" 2>/dev/null)"
[ -n "$last" ] || { echo null; return 0; }
IFS='|' read -r at m b bo bt co cn res <<-ZA_EOF
$last
ZA_EOF
printf '{"at":%s,"mode":"%s","best":"%s","best_ok":%s,"total":%s,"cur_ok":%s,"cur":"%s","result":"%s"}' \
"$(printf '%s' "$at" | tr -cd 0-9 | grep . || echo 0)" "$(esc "$m")" "$(esc "$b")" "$(printf '%s' "$bo" | tr -cd 0-9 | grep . || echo 0)" \
"$(printf '%s' "$bt" | tr -cd 0-9 | grep . || echo 0)" "$(printf '%s' "$co" | tr -cd 0-9 | grep . || echo 0)" "$(esc "$cn")" "$(esc "$res")"
}
_za_json() { _za_json() {
local h last at m b bo bt co cn res local h last lasty
h="$(_za_time)" h="$(_za_time)"
last="$(cat "$ZA_LAST" 2>/dev/null)" last="$(_za_last_json "$ZA_LAST")"
if [ -n "$last" ]; then lasty="$(_za_last_json "$ZA_LAST_YT")"
IFS='|' read -r at m b bo bt co cn res <<-ZA_EOF
$last
ZA_EOF
last="{\"at\":${at:-0},\"mode\":\"$(esc "$m")\",\"best\":\"$(esc "$b")\",\"best_ok\":${bo:-0},\"total\":${bt:-0},\"cur_ok\":${co:-0},\"cur\":\"$(esc "$cn")\",\"result\":\"$(esc "$res")\"}"
else last=null; fi
local tsat="" tsp="" l local tsat="" tsp="" l
tsat="$(cat "$ZA_TS" 2>/dev/null | tr -cd 0-9)" tsat="$(cat "$ZA_TS" 2>/dev/null | tr -cd 0-9)"
if [ -n "$tsat" ] && [ -f "$CONF" ]; then if [ -n "$tsat" ] && [ -f "$CONF" ]; then
for l in $(_nfq_opt_body | _ts_parts | tr ' ' '_'); do tsp="$tsp${tsp:+,}\"$(esc "$(printf '%s' "$l" | tr '_' ' ')")\""; done for l in $(_nfq_opt_body | _ts_parts | tr ' ' '_'); do tsp="$tsp${tsp:+,}\"$(esc "$(printf '%s' "$l" | tr '_' ' ')")\""; done
[ -n "$tsp" ] || tsat="" [ -n "$tsp" ] || tsat=""
fi fi
printf '{"time":"%s","pref":"%s","mode":"%s","running":%s,"last":%s,"ts_at":%s,"ts_parts":[%s]}' "$h" "$(cat "$ZA_TIME_FILE" 2>/dev/null || echo 04:00)" "$(_za_mode)" "$(_za_running && echo true || echo false)" "$last" "${tsat:-0}" "$tsp" printf '{"time":"%s","pref":"%s","mode":"%s","yt":%s,"running":%s,"last":%s,"last_yt":%s,"ts_at":%s,"ts_parts":[%s]}' "$h" "$(cat "$ZA_TIME_FILE" 2>/dev/null || echo 04:00)" "$(_za_mode)" "$(_za_yt && echo true || echo false)" "$(_za_running && echo true || echo false)" "$last" "$lasty" "${tsat:-0}" "$tsp"
} }
_zt_tz() { _zt_tz() {
@@ -10332,10 +10408,20 @@ stl_diag_lines() {
rm -f "$f" rm -f "$f"
} }
_stl_explain_run() {
local t="" x="$STL_ETC/.zm-explain.$$.json"
if [ -s "$STL_SPEC" ] && grep -q '"scope":"device"' "$STL_SPEC" 2>/dev/null; then
sed 's/"scope":"device"/"scope":"device","enabled":false/g' "$STL_SPEC" > "$x" 2>/dev/null && t="$(_stl_t 20 steer explain --spec "$x" "$1" 2>&1)"
rm -f "$x"
case "$t" in *' -> '*) printf '%s\n' "$t"; return 0 ;; esac
fi
_stl_t 20 steer explain "$1" 2>&1
}
stl_explain() { stl_explain() {
local t line local t line
STL_X_VERDICT=unknown; STL_X_OUT=""; STL_X_DEV=""; STL_X_SET=""; STL_X_ADDR=""; STL_X_FAKE=false STL_X_VERDICT=unknown; STL_X_OUT=""; STL_X_DEV=""; STL_X_SET=""; STL_X_ADDR=""; STL_X_FAKE=false
t="$(_stl_t 20 steer explain "$1" 2>&1)" t="$(_stl_explain_run "$1")"
STL_X_TEXT="$t" STL_X_TEXT="$t"
line="$(printf '%s\n' "$t" | grep -m1 -- '-> output "')" line="$(printf '%s\n' "$t" | grep -m1 -- '-> output "')"
STL_X_ADDR="$(printf '%s\n' "$t" | head -n1 | sed -n 's/^[^ ]* -> \([0-9a-fA-F.:]*\) (.*/\1/p')" STL_X_ADDR="$(printf '%s\n' "$t" | head -n1 | sed -n 's/^[^ ]* -> \([0-9a-fA-F.:]*\) (.*/\1/p')"
@@ -17008,8 +17094,20 @@ _tsc_login() {
return 1 return 1
} }
_tsc_route_ok() {
{ printf '%s' "$1" | jsonfilter -e '@.Self.PrimaryRoutes[*]' 2>/dev/null; printf '%s' "$1" | jsonfilter -e '@.Self.AllowedIPs[*]' 2>/dev/null; } |
awk -v want="$2" '
function ipn(a, o) { if (split(a, o, ".") != 4) return -1; return ((o[1] * 256 + o[2]) * 256 + o[3]) * 256 + o[4] }
BEGIN { split(want, w, "/"); wa = ipn(w[1]); wm = (w[2] == "" ? 32 : w[2] + 0) }
$0 !~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+$/ { next }
{ split($0, r, "/"); m = r[2] + 0; if (m < 1 || m > wm) next; z = 2 ^ (32 - m); if (int(ipn(r[1]) / z) == int(wa / z)) { ok = 1; exit } }
END { exit !ok }'
}
TSC_MANUAL="$TSC_DIR/approved.manual"
tsc_status() { tsc_status() {
local inst=false run=false en=false on=false j="" st="" url="" ip="" host="" tn="" exo=false lano=false local inst=false run=false en=false on=false j="" st="" url="" ip="" host="" tn="" exo=false lano=false man=false
local pok=true cidr="" ver="" kexp="" lip lprev="" net=false zone=false nft=false tg=false tgr=0 zr=false st_on=false z lz free local pok=true cidr="" ver="" kexp="" lip lprev="" net=false zone=false nft=false tg=false tgr=0 zr=false st_on=false z lz free
_tsc_installed && { inst=true; ver="$(_tsc_ver)"; } _tsc_installed && { inst=true; ver="$(_tsc_ver)"; }
[ -f "$TSC_ON" ] && on=true [ -f "$TSC_ON" ] && on=true
@@ -17029,7 +17127,11 @@ tsc_status() {
[ "$(printf '%s' "$j" | jsonfilter -e '@.Self.ExitNodeOption' 2>/dev/null)" = true ] && exo=true [ "$(printf '%s' "$j" | jsonfilter -e '@.Self.ExitNodeOption' 2>/dev/null)" = true ] && exo=true
printf '%s' "$j" | jsonfilter -e '@.Self.AllowedIPs[*]' 2>/dev/null | grep -qx '0.0.0.0/0' && exo=true printf '%s' "$j" | jsonfilter -e '@.Self.AllowedIPs[*]' 2>/dev/null | grep -qx '0.0.0.0/0' && exo=true
cidr="$(_tsc_lan_cidr)" cidr="$(_tsc_lan_cidr)"
[ -n "$cidr" ] && printf '%s' "$j" | jsonfilter -e '@.Self.PrimaryRoutes[*]' 2>/dev/null | grep -qxF "$cidr" && lano=true [ -n "$cidr" ] && _tsc_route_ok "$j" "$cidr" && lano=true
[ "$st" = Running ] && [ -s "$TSC_MANUAL" ] && {
! $exo && grep -qx 'exit' "$TSC_MANUAL" && exo=true && man=true
! $lano && [ -n "$cidr" ] && grep -qxF "lan $cidr" "$TSC_MANUAL" && lano=true && man=true
}
if [ "$st" = Running ] && [ -f "$TSC_ON" ] && ! _job_alive tsc; then if [ "$st" = Running ] && [ -f "$TSC_ON" ] && ! _job_alive tsc; then
local dp local dp
dp="$(_tsc_dbg)" dp="$(_tsc_dbg)"
@@ -17068,6 +17170,7 @@ tsc_status() {
local lat="" vcur local lat="" vcur
$inst && lat="$(_zm_cached tsc _tsc_gh_latest)" $inst && lat="$(_zm_cached tsc _tsc_gh_latest)"
vcur="$(_tsc_vnum "$ver")" vcur="$(_tsc_vnum "$ver")"
printf '"manual_ok":%s,' "$man"
printf '"prefs_ok":%s,"luci_path":"%s","key_expiry":"%s","lan_prev":"%s","term_mode":"%s",' "$pok" "$(esc "$(_tsc_luci_path)")" "$(esc "$kexp")" "$(esc "$lprev")" "$(_term_installed && _term_mode)" printf '"prefs_ok":%s,"luci_path":"%s","key_expiry":"%s","lan_prev":"%s","term_mode":"%s",' "$pok" "$(esc "$(_tsc_luci_path)")" "$(esc "$kexp")" "$(esc "$lprev")" "$(_term_installed && _term_mode)"
printf '"fk":%s,"fk_ok":%s,"mx":%s,"adblock":%s,' "$(_fk_installed && echo true || echo false)" "$(_tsc_fk_has && echo true || echo false)" \ printf '"fk":%s,"fk_ok":%s,"mx":%s,"adblock":%s,' "$(_fk_installed && echo true || echo false)" "$(_tsc_fk_has && echo true || echo false)" \
"$([ -x /etc/init.d/magitrickle ] && echo true || echo false)" "$(_ab_on && echo true || echo false)" "$([ -x /etc/init.d/magitrickle ] && echo true || echo false)" "$(_ab_on && echo true || echo false)"
@@ -17206,20 +17309,26 @@ tsc_action() {
repair) job_start tsc do_tsc_repair ;; repair) job_start tsc do_tsc_repair ;;
update) job_start tsc do_tsc_update ;; update) job_start tsc do_tsc_update ;;
esac ;; esac ;;
login|logout|restart|exit|lan|server|apply|kick|lanack) login|logout|restart|exit|lan|server|apply|kick|lanack|approved)
_tsc_installed || { echo '{"error":"Tailscale не установлен"}'; return 1; } _tsc_installed || { echo '{"error":"Tailscale не установлен"}'; return 1; }
_job_alive tsc && { echo '{"error":"дождитесь окончания текущей операции"}'; return 1; } _job_alive tsc && { echo '{"error":"дождитесь окончания текущей операции"}'; return 1; }
case "$a" in case "$a" in
login) _tsc_login ;; login) _tsc_login ;;
server) _tsc_server_set "$m" ;; server) rm -f "$TSC_MANUAL"; _tsc_server_set "$m" ;;
kick) _tsc_kick_bg; printf '{"ok":true}\n' ;; kick) _tsc_kick_bg; printf '{"ok":true}\n' ;;
lanack) mkdir -p "$TSC_DIR"; _zm_lan_ip > "$TSC_DIR/lan.ip"; printf '{"ok":true}\n' ;; lanack) mkdir -p "$TSC_DIR"; _zm_lan_ip > "$TSC_DIR/lan.ip"; printf '{"ok":true}\n' ;;
approved)
mkdir -p "$TSC_DIR"
if [ "$m" = off ]; then rm -f "$TSC_MANUAL"; printf '{"ok":true}\n'; return 0; fi
{ [ -f "$TSC_NOEXIT" ] || echo exit; [ -f "$TSC_LAN" ] && _tsc_lan_cidr | sed 's/^/lan /'; } > "$TSC_MANUAL"
printf '{"ok":true}\n' ;;
apply) apply)
_tsc_prefs _tsc_prefs
[ -n "$TSC_KICK" ] && _tsc_kick_bg [ -n "$TSC_KICK" ] && _tsc_kick_bg
if _tsc_prefs_ok; then printf '{"ok":true}\n' if _tsc_prefs_ok; then printf '{"ok":true}\n'
else echo '{"error":"Tailscale не принял настройки — нажмите «Перезапустить» и попробуйте ещё раз"}'; return 1; fi ;; else echo '{"error":"Tailscale не принял настройки — нажмите «Перезапустить» и попробуйте ещё раз"}'; return 1; fi ;;
logout) logout)
rm -f "$TSC_MANUAL"
pkill -f 'tailscale (up|login)' >/dev/null 2>&1 pkill -f 'tailscale (up|login)' >/dev/null 2>&1
_zm_tmo 15 tailscale logout >/dev/null 2>&1 || { echo '{"error":"не получилось выйти из аккаунта — смотрите журнал роутера"}'; return 1; } _zm_tmo 15 tailscale logout >/dev/null 2>&1 || { echo '{"error":"не получилось выйти из аккаунта — смотрите журнал роутера"}'; return 1; }
printf '{"ok":true}\n' ;; printf '{"ok":true}\n' ;;
@@ -17238,6 +17347,7 @@ tsc_action() {
: > "$TSC_LAN" : > "$TSC_LAN"
else rm -f "$TSC_LAN"; fi else rm -f "$TSC_LAN"; fi
fi fi
[ "$m" = off ] && rm -f "$TSC_MANUAL"
_tsc_prefs || { echo '{"error":"Tailscale не принял настройку — смотрите журнал роутера"}'; return 1; } _tsc_prefs || { echo '{"error":"Tailscale не принял настройку — смотрите журнал роутера"}'; return 1; }
printf '{"ok":true}\n' ;; printf '{"ok":true}\n' ;;
esac ;; esac ;;
@@ -24648,7 +24758,7 @@ cat > '/usr/share/luci/menu.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF
"action": { "type": "view", "path": "zapret-manager/awg" } "action": { "type": "view", "path": "zapret-manager/awg" }
}, },
"admin/services/zapret-manager/tailscale": { "admin/services/zapret-manager/tailscale": {
"title": "Tailscale", "title": "Удалённый доступ",
"order": 54, "order": 54,
"action": { "type": "view", "path": "zapret-manager/tailscale" } "action": { "type": "view", "path": "zapret-manager/tailscale" }
}, },
@@ -28775,8 +28885,7 @@ var E = (function(raw) { return function() { var a = Array.prototype.slice.call(
var ROUTERICH = 'https://rc.routerich.ru'; var ROUTERICH = 'https://rc.routerich.ru';
var ROUTERICH_NEW = 'https://remote.routerich.ru/create-tailnet'; var ROUTERICH_NEW = 'https://remote.routerich.ru/create-tailnet';
var APK_URL = 'https://github.com/tailscale/tailscale-android/releases'; var APK_URL = 'https://github.com/tailscale/tailscale-android/releases/latest';
var PLAY_URL = 'https://play.google.com/store/apps/details?id=com.tailscale.ipn';
var IOS_URL = 'https://apps.apple.com/app/tailscale/id1470499037'; var IOS_URL = 'https://apps.apple.com/app/tailscale/id1470499037';
var TESTFLIGHT_URL = 'https://testflight.apple.com/join/tLcYLZJV'; var TESTFLIGHT_URL = 'https://testflight.apple.com/join/tLcYLZJV';
var TAILSOCKS_URL = 'https://github.com/bropines/tailsocks/releases/latest'; var TAILSOCKS_URL = 'https://github.com/bropines/tailsocks/releases/latest';
@@ -29026,8 +29135,8 @@ return view.extend({
function renderIntro() { function renderIntro() {
var free = +data.free_kb || 0, need = +data.need_kb || 20480, low = free > 0 && free < need; var free = +data.free_kb || 0, need = +data.need_kb || 20480, low = free > 0 && free < need;
mainCard.appendChild(E('h3', {}, 'Tailscale')); mainCard.appendChild(E('h3', {}, 'Удалённый доступ'));
mainCard.appendChild(E('p', { 'class': 'zm-hint', 'style': 'margin-top:-6px' }, 'Телефон в любой сети — мобильный интернет, кафе, поездка — выходит в интернет через домашний роутер. С ним работают Zapret, Steer и sTGWS, как дома. Белый IP не нужен: телефон и роутер находят друг друга сами.')); mainCard.appendChild(E('p', { 'class': 'zm-hint', 'style': 'margin-top:-6px' }, 'Работает на Tailscale. Телефон в любой сети — мобильный интернет, кафе, поездка — выходит в интернет через домашний роутер. С ним работают Zapret, Steer и sTGWS, как дома. Белый IP не нужен: телефон и роутер находят друг друга сами.'));
mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Всё ставится с GitHub: уменьшенная сборка Tailscale для OpenWrt и страница для LuCI. Сайт Tailscale из России не открывается, поэтому подключиться можно через Routerich — по ключу, без аккаунта.')); mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Всё ставится с GitHub: уменьшенная сборка Tailscale для OpenWrt и страница для LuCI. Сайт Tailscale из России не открывается, поэтому подключиться можно через Routerich — по ключу, без аккаунта.'));
mainCard.appendChild(E('div', { 'class': 'zm-kv' }, [ mainCard.appendChild(E('div', { 'class': 'zm-kv' }, [
row('Нужно', E('span', {}, 'около ' + Math.round(need / 1024) + ' МБ флеш-памяти')), row('Нужно', E('span', {}, 'около ' + Math.round(need / 1024) + ' МБ флеш-памяти')),
@@ -29041,7 +29150,7 @@ return view.extend({
function renderMain() { function renderMain() {
mainCard.innerHTML = ''; mainCard.innerHTML = '';
if (!data.installed) { renderIntro(); return; } if (!data.installed) { renderIntro(); return; }
mainCard.appendChild(E('h3', {}, 'Tailscale')); mainCard.appendChild(E('h3', {}, 'Удалённый доступ'));
var st = data.state || '', kv = E('div', { 'class': 'zm-kv' }); var st = data.state || '', kv = E('div', { 'class': 'zm-kv' });
kv.appendChild(row('Служба', data.running ? badge('zm-ok', 'работает') : badge(data.enabled ? 'zm-bad' : 'zm-off', 'остановлена'))); kv.appendChild(row('Служба', data.running ? badge('zm-ok', 'работает') : badge(data.enabled ? 'zm-bad' : 'zm-off', 'остановлена')));
kv.appendChild(row('Сервер', E('span', { 'style': 'display:inline-flex; align-items:center; gap:8px; flex-wrap:wrap' }, [ kv.appendChild(row('Сервер', E('span', { 'style': 'display:inline-flex; align-items:center; gap:8px; flex-wrap:wrap' }, [
@@ -29061,6 +29170,10 @@ return view.extend({
kv.appendChild(row('Выход в интернет', !data.exit_want ? badge('zm-off', 'выключен') : kv.appendChild(row('Выход в интернет', !data.exit_want ? badge('zm-off', 'выключен') :
data.exit_ok ? badge('zm-ok', 'готов — выберите роутер на телефоне') : badge('zm-warn', 'ждёт одобрения на сервере'))); data.exit_ok ? badge('zm-ok', 'готов — выберите роутер на телефоне') : badge('zm-warn', 'ждёт одобрения на сервере')));
if (data.lan_want) kv.appendChild(row('Домашняя сеть', data.lan_ok ? badge('zm-ok', 'доступна · ' + data.lan_cidr) : badge('zm-warn', 'ждёт одобрения · ' + data.lan_cidr))); if (data.lan_want) kv.appendChild(row('Домашняя сеть', data.lan_ok ? badge('zm-ok', 'доступна · ' + data.lan_cidr) : badge('zm-warn', 'ждёт одобрения · ' + data.lan_cidr)));
if (data.manual_ok) kv.appendChild(row('Одобрение', E('span', { 'style': 'display:inline-flex; align-items:center; gap:8px; flex-wrap:wrap' }, [
E('span', { 'class': 'zm-hint', 'style': 'margin:0' }, 'отмечено вручную'),
E('button', { 'type': 'button', 'class': 'cbi-button', 'style': 'padding:2px 10px', 'disabled': busy ? '' : null, 'click': function() { quick('approved', 'off', 'Отметка снята — панель снова проверяет сервер'); } }, 'Снять отметку')
])));
kv.appendChild(row('Вход роутера', keyExp() ? badge(keyDays() < 30 ? 'zm-bad' : 'zm-warn', 'истекает ' + keyExp()) : badge('zm-ok', 'бессрочный'))); kv.appendChild(row('Вход роутера', keyExp() ? badge(keyDays() < 30 ? 'zm-bad' : 'zm-warn', 'истекает ' + keyExp()) : badge('zm-ok', 'бессрочный')));
} }
var srcName = data.source === 'github' ? ' · сборка с GitHub' : data.source === 'openwrt' ? ' · из репозитория OpenWrt' : ''; var srcName = data.source === 'github' ? ' · сборка с GitHub' : data.source === 'openwrt' ? ' · из репозитория OpenWrt' : '';
@@ -29186,7 +29299,16 @@ return view.extend({
? (data.lan_want && !data.lan_ok ? [ 'Нажмите ', E('b', {}, 'Маршруты'), ', отметьте подсеть ', E('b', {}, data.lan_cidr), ' и сохраните' ] : null) ? (data.lan_want && !data.lan_ok ? [ 'Нажмите ', E('b', {}, 'Маршруты'), ', отметьте подсеть ', E('b', {}, data.lan_cidr), ' и сохраните' ] : null)
: [ whatNode ] : [ whatNode ]
].filter(Boolean)), ].filter(Boolean)),
E('span', { 'class': 'zm-hint', 'style': 'margin:0' }, 'Статус здесь обновится в течение минуты.') E('span', { 'class': 'zm-hint', 'style': 'margin:0' }, 'Статус здесь обновится в течение минуты. Уже одобрили, телефон видит домашнюю сеть и выходит в интернет через роутер, а надпись не уходит? Часть серверов не сообщает роутеру об одобрении — отметьте вручную.'),
E('div', {}, [ E('button', { 'type': 'button', 'class': 'cbi-button', 'disabled': busy ? '' : null, 'click': function() {
zm.dialog({ title: 'Всё уже работает?', okText: 'Да, всё работает',
blocks: [ { type: 'list', title: 'Отметьте, только если с телефона через Tailscale', items: [
data.lan_want && !data.lan_ok ? 'открываются устройства домашней сети (например, LuCI по адресу ' + (data.lan_ip || 'роутера') + ')' : null,
data.exit_want && !data.exit_ok ? 'работает выход в интернет через роутер' : null,
'Панель перестанет показывать «ждёт одобрения»; сменится домашняя подсеть — отметка сбросится сама'
].filter(Boolean) } ]
}).then(function(v) { if (v) quick('approved', 'on', 'Отмечено: роутер одобрен на сервере'); });
} }, 'У меня всё работает') ])
])); ]));
} }
@@ -29269,7 +29391,7 @@ return view.extend({
if (useLan && !data.lan_ok) phoneCard.appendChild(E('p', { 'class': 'zm-hint zm-ab-warn' }, 'Домашняя сеть ' + (data.lan_cidr || '') + ' ещё не одобрена на сервере — без этого телефон не достучится до ' + dnsIp + '. Одобрите её (см. выше).')); if (useLan && !data.lan_ok) phoneCard.appendChild(E('p', { 'class': 'zm-hint zm-ab-warn' }, 'Домашняя сеть ' + (data.lan_cidr || '') + ' ещё не одобрена на сервере — без этого телефон не достучится до ' + dnsIp + '. Одобрите её (см. выше).'));
phoneCard.appendChild(E('h4', { 'style': 'margin:14px 0 6px' }, '2. На телефоне')); phoneCard.appendChild(E('h4', { 'style': 'margin:14px 0 6px' }, '2. На телефоне'));
var android = [ E('b', {}, 'Android'), ': ', ext(PLAY_URL, 'Google Play'), ' или ', ext(APK_URL, 'APK с GitHub'), ' (файл tailscale-android-universal). Или ', E('b', {}, 'TailSocks'), ' — неофициальный клиент Tailscale с открытым кодом (', ext(TAILSOCKS_URL, 'APK на GitHub'), ')' ]; var android = [ E('b', {}, 'Android'), ': ', ext(APK_URL, 'Tailscale'), ' (файл tailscale-android-universal) или ', ext(TAILSOCKS_URL, 'TailSocks'), ' — неофициальный клиент Tailscale с открытым кодом' ];
var iphone = [ E('b', {}, 'iPhone'), ': ', ext(IOS_URL, 'App Store'), ', а если его там нет — ', ext(TESTFLIGHT_URL, 'TestFlight') ]; var iphone = [ E('b', {}, 'iPhone'), ': ', ext(IOS_URL, 'App Store'), ', а если его там нет — ', ext(TESTFLIGHT_URL, 'TestFlight') ];
phoneCard.appendChild(steps([ phoneCard.appendChild(steps([
[ 'Установите приложение ', E('b', {}, 'Tailscale'), '. ' ].concat(android, [ '. ' ], iphone), [ 'Установите приложение ', E('b', {}, 'Tailscale'), '. ' ].concat(android, [ '. ' ], iphone),
@@ -29320,7 +29442,7 @@ return view.extend({
function renderLoading() { function renderLoading() {
mainCard.innerHTML = ''; mainCard.innerHTML = '';
mainCard.appendChild(E('h3', {}, 'Tailscale')); mainCard.appendChild(E('h3', {}, 'Удалённый доступ'));
if (loadErr) { if (loadErr) {
mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Роутер не ответил: ' + loadErr)); mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Роутер не ответил: ' + loadErr));
mainCard.appendChild(E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button cbi-button-action', 'click': firstLoad }, 'Повторить') ])); mainCard.appendChild(E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button cbi-button-action', 'click': firstLoad }, 'Повторить') ]));
@@ -33355,7 +33477,7 @@ var TEST_MODE_LABELS = {
var TEST_RESULT_MODES = ['current', 'domain', 'v', 'flowseal', 'v_flowseal', 'youtube', 'custom', 'custom_yt']; var TEST_RESULT_MODES = ['current', 'domain', 'v', 'flowseal', 'v_flowseal', 'youtube', 'custom', 'custom_yt'];
var ZONE_RU = { 'Europe/Kaliningrad': 'Калининград', 'Europe/Moscow': 'Москва', 'Europe/Samara': 'Самара', 'Asia/Yekaterinburg': 'Екатеринбург', 'Asia/Omsk': 'Омск', 'Asia/Krasnoyarsk': 'Красноярск', var ZONE_RU = { 'Europe/Kaliningrad': 'Калининград', 'Europe/Moscow': 'Москва', 'Europe/Samara': 'Самара', 'Asia/Yekaterinburg': 'Екатеринбург', 'Asia/Omsk': 'Омск', 'Asia/Krasnoyarsk': 'Красноярск',
'Asia/Irkutsk': 'Иркутск', 'Asia/Yakutsk': 'Якутск', 'Asia/Vladivostok': 'Владивосток', 'Asia/Magadan': 'Магадан', 'Asia/Kamchatka': 'Камчатка', 'UTC': 'UTC' }; 'Asia/Irkutsk': 'Иркутск', 'Asia/Yakutsk': 'Якутск', 'Asia/Vladivostok': 'Владивосток', 'Asia/Magadan': 'Магадан', 'Asia/Kamchatka': 'Камчатка', 'UTC': 'UTC' };
var AUTO_MODES = [ { id: 'v', label: 'v1–v10' }, { id: 'flowseal', label: 'Flowseal' }, { id: 'v_flowseal', label: 'v + Flowseal' } ]; var AUTO_MODES = [ { id: 'v', label: 'v1–v10' }, { id: 'flowseal', label: 'Flowseal' }, { id: 'v_flowseal', label: 'v + Flowseal' }, { id: 'youtube', label: 'Только YouTube' } ];
var CUSTOM_SCRIPTS = [ var CUSTOM_SCRIPTS = [
{ id: 'off', label: 'Нет' }, { id: 'off', label: 'Нет' },
{ id: 'stun4all', label: '50-stun4all', hint: 'обход для STUN на всех портах — голос и видео в Discord, Telegram, WhatsApp и других' }, { id: 'stun4all', label: '50-stun4all', hint: 'обход для STUN на всех портах — голос и видео в Discord, Telegram, WhatsApp и других' },
@@ -33930,10 +34052,10 @@ return view.extend({
var mins = (+t.slice(0, 2)) * 60 + (+t.slice(3, 5)), cur = now.getUTCHours() * 60 + now.getUTCMinutes(); var mins = (+t.slice(0, 2)) * 60 + (+t.slice(3, 5)), cur = now.getUTCHours() * 60 + now.getUTCMinutes();
return (mins > cur ? 'сегодня в ' : 'завтра в ') + t; return (mins > cur ? 'сегодня в ' : 'завтра в ') + t;
} }
function autoSet(when, mode, okText) { function autoSet(when, mode, okText, yt) {
if (autoBusy) return; if (autoBusy) return;
autoBusy = true; renderAuto(); autoBusy = true; renderAuto();
zm.testAction('auto_set', when + '|' + mode).then(function(r) { zm.testAction('auto_set', when + '|' + mode + (yt ? '|' + yt : '')).then(function(r) {
autoBusy = false; autoBusy = false;
if (!r || r.error) { zm.toast((r && r.error) || 'Роутер не ответил', 'error'); renderAuto(); return; } if (!r || r.error) { zm.toast((r && r.error) || 'Роутер не ответил', 'error'); renderAuto(); return; }
autoInfo = r.auto || {}; timeInfo = r; autoInfo = r.auto || {}; timeInfo = r;
@@ -33942,11 +34064,11 @@ return view.extend({
}).catch(function() { autoBusy = false; zm.toast('Роутер не ответил', 'error'); renderAuto(); }); }).catch(function() { autoBusy = false; zm.toast('Роутер не ответил', 'error'); renderAuto(); });
} }
function renderAuto() { function renderAuto() {
var on = !!autoInfo.time, mode = autoInfo.mode || 'v_flowseal', t = autoInfo.time || autoInfo.pref || '04:00'; var on = !!autoInfo.time, mode = autoInfo.mode || 'v_flowseal', t = autoInfo.time || autoInfo.pref || '04:00', yt = !!autoInfo.yt;
var preset = AUTO_TIMES.indexOf(t) >= 0 && !autoOwn; var preset = AUTO_TIMES.indexOf(t) >= 0 && !autoOwn;
autoCard.innerHTML = ''; autoCard.innerHTML = '';
autoCard.appendChild(E('h3', {}, [ 'Автоподбор стратегии ', on ? zm.badge(true, 'ежедневно в ' + autoInfo.time, '') : E([]) ])); autoCard.appendChild(E('h3', {}, [ 'Автоподбор стратегии ', on ? zm.badge(true, 'ежедневно в ' + autoInfo.time, '') : E([]) ]));
autoCard.appendChild(E('p', { 'class': 'zm-hint', 'style': 'margin-top:-6px' }, 'Роутер проверяет стратегии на списке заблокированных сайтов и сравнивает лучшую с текущей. Новая стратегия применяется, только если открывает больше сайтов. Настройки YouTube, Discord, игр, QUIC и дополнения к стратегии сохраняются.')); autoCard.appendChild(E('p', { 'class': 'zm-hint', 'style': 'margin-top:-6px' }, 'Роутер проверяет стратегии на списке заблокированных сайтов и сравнивает лучшую с текущей. Новая стратегия применяется, только если открывает больше сайтов. Настройки Discord, игр, QUIC и дополнения к стратегии сохраняются, YouTube — тоже, если не включён подбор для него.'));
autoCard.appendChild(zm.swRow(on, 'Запускать ежедневно', on ? 'Следующий запуск: ' + nextText(autoInfo.time) + '. Проверка длится 5–10 минут, Zapret в это время перезапускается.' : 'Выключено — стратегия меняется только вручную.', function() { autoCard.appendChild(zm.swRow(on, 'Запускать ежедневно', on ? 'Следующий запуск: ' + nextText(autoInfo.time) + '. Проверка длится 5–10 минут, Zapret в это время перезапускается.' : 'Выключено — стратегия меняется только вручную.', function() {
if (autoBusy) return; if (autoBusy) return;
autoSet(on ? 'off' : t, mode); autoSet(on ? 'off' : t, mode);
@@ -33964,19 +34086,29 @@ return view.extend({
} }, 'Сохранить')); } }, 'Сохранить'));
} }
autoCard.appendChild(E('div', { 'class': 'zm-row zm-ab-row' }, [ E('span', { 'class': 'zm-label' }, 'Время запуска'), E('div', { 'class': 'zm-ab-ctl' }, ctl) ])); autoCard.appendChild(E('div', { 'class': 'zm-row zm-ab-row' }, [ E('span', { 'class': 'zm-label' }, 'Время запуска'), E('div', { 'class': 'zm-ab-ctl' }, ctl) ]));
autoCard.appendChild(E('div', { 'class': 'zm-row zm-ab-row' }, [ E('span', { 'class': 'zm-label' }, 'Стратегии'), }
E('div', { 'class': 'zm-seg' + (autoBusy ? ' zm-seg-busy' : '') }, AUTO_MODES.map(function(m) { autoCard.appendChild(E('div', { 'class': 'zm-row zm-ab-row' }, [ E('span', { 'class': 'zm-label' }, 'Стратегии'),
return E('div', { 'class': 'zm-seg-item' + (m.id === mode ? ' zm-active' : ''), 'click': function() { if (!autoBusy && m.id !== mode) autoSet(autoInfo.time, m.id, 'Автоподбор будет проверять: ' + m.label); } }, m.label); E('div', { 'class': 'zm-seg' + (autoBusy ? ' zm-seg-busy' : '') }, AUTO_MODES.map(function(m) {
})) return E('div', { 'class': 'zm-seg-item' + (m.id === mode ? ' zm-active' : ''), 'click': function() { if (!autoBusy && m.id !== mode) autoSet(autoInfo.time || 'off', m.id, 'Автоподбор будет проверять: ' + (m.id === 'youtube' ? 'только стратегии YouTube (Yv)' : m.label + (yt ? ' и YouTube' : ''))); } }, m.label);
])); }))
]));
if (mode !== 'youtube') autoCard.appendChild(zm.swRow(yt, 'Подбирать и стратегию для YouTube',
yt ? 'Сначала подбирается основная стратегия, затем для YouTube (Yv) — на адресах YouTube. Новая ставится, только если открывает больше.' : 'Выключено — настройка YouTube остаётся как есть.',
function() { if (!autoBusy) autoSet(autoInfo.time || 'off', mode, yt ? 'Подбор для YouTube выключен' : 'Включено: после основной стратегии — подбор для YouTube', yt ? 'off' : 'on'); }, autoBusy));
else autoCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Меняется только стратегия YouTube (Yv), основная стратегия остаётся как есть.'));
function lastRes(l) {
var yv = l.mode === 'youtube', unit = yv ? ' адресов YouTube' : ' сайтов', cur = l.cur || (yv ? 'из основной стратегии' : 'текущая');
return l.result === 'applied' ? [ E('b', { 'class': 'zm-ab-ok' }, 'применена ' + l.best), ' — ' + l.best_ok + ' из ' + l.total + unit + ' (было ' + cur + ' — ' + l.cur_ok + ' из ' + l.total + ')' ]
: l.result === 'kept' ? [ E('b', {}, 'оставлена ' + cur), ' — ' + l.cur_ok + ' из ' + l.total + unit + '; лучшая из проверенных ' + l.best + ' — ' + l.best_ok + ' из ' + l.total ]
: l.result === 'nochange' ? [ 'пропущен — включено «Не изменять стратегию»' ]
: l.result === 'yoff' ? [ 'пропущен — YouTube выключен в стратегии' ]
: l.result === 'none' ? [ 'лучшую стратегию определить не удалось, оставлена текущая' ] : [ E('b', { 'class': 'zm-ab-bad' }, 'ошибка'), ' — подробности в журнале теста' ];
} }
var last = autoInfo.last; var last = autoInfo.last;
if (last && last.at) { if (last && last.at) {
var res = last.result === 'applied' ? [ E('b', { 'class': 'zm-ab-ok' }, 'применена ' + last.best), ' — ' + last.best_ok + ' из ' + last.total + ' сайтов (было ' + (last.cur || 'текущая') + ' — ' + last.cur_ok + ' из ' + last.total + ')' ] var ly = autoInfo.last_yt, lastEl = E('div', { 'class': 'zm-ab-last' }, [ E('span', { 'class': 'zm-label' }, 'Последний запуск ' + whenText(last.at) + (last.mode === 'youtube' ? ', YouTube: ' : ': ')) ].concat(lastRes(last)));
: last.result === 'kept' ? [ E('b', {}, 'оставлена ' + (last.cur || 'текущая')), ' — ' + last.cur_ok + ' из ' + last.total + ' сайтов; лучшая из проверенных ' + last.best + ' — ' + last.best_ok + ' из ' + last.total ] if (ly && ly.at && last.mode !== 'youtube') lastEl.appendChild(E('div', { 'style': 'margin-top:6px' }, [ E('span', { 'class': 'zm-label' }, 'YouTube: ') ].concat(lastRes(ly))));
: last.result === 'nochange' ? [ 'пропущен — включено «Не изменять стратегию»' ] autoCard.appendChild(lastEl);
: last.result === 'none' ? [ 'лучшую стратегию определить не удалось, оставлена текущая' ] : [ E('b', { 'class': 'zm-ab-bad' }, 'ошибка'), ' — подробности в журнале теста' ];
autoCard.appendChild(E('div', { 'class': 'zm-ab-last' }, [ E('span', { 'class': 'zm-label' }, 'Последний запуск ' + whenText(last.at) + ': ') ].concat(res)));
} }
var yr = timeInfo.time ? new Date(timeInfo.time * 1000).getFullYear() : 0; var yr = timeInfo.time ? new Date(timeInfo.time * 1000).getFullYear() : 0;
if (on && yr && yr < 2024) autoCard.appendChild(E('p', { 'class': 'zm-hint' }, '⚠ Время на роутере неверное — расписание не сработает. Настройте его на странице «Система».')); if (on && yr && yr < 2024) autoCard.appendChild(E('p', { 'class': 'zm-hint' }, '⚠ Время на роутере неверное — расписание не сработает. Настройте его на странице «Система».'));
@@ -35430,6 +35562,14 @@ html.zm-theme-dark .zm-list-dirty { color: #d29922; }
margin-top: 12px; margin-top: 12px;
} }
.zm-refresh-banner button { flex-shrink: 0; } .zm-refresh-banner button { flex-shrink: 0; }
.zm-refresh-banner:not(.zm-reboot-banner) { flex-wrap: wrap; }
.zm-refresh-banner:not(.zm-reboot-banner) > span:first-child { flex: 1 1 260px; min-width: 0; line-height: 1.45; }
@media (max-width: 640px) {
.zm-refresh-banner:not(.zm-reboot-banner) { padding: 12px 14px; font-size: 14px; gap: 10px 12px; }
.zm-refresh-banner:not(.zm-reboot-banner) > span:first-child { flex-basis: 100%; }
.zm-refresh-banner:not(.zm-reboot-banner) > .cbi-button { flex: 1 1 auto; margin: 0; min-width: 0; white-space: normal; }
.zm-refresh-banner:not(.zm-reboot-banner) > .cbi-button.zm-cl-link { flex: 0 0 auto; }
}
.zm-ab-auto { margin-top: 16px; border: 1px solid rgba(127,127,127,.22); border-radius: 12px; padding: 12px 14px; background: rgba(127,127,127,.05); } .zm-ab-auto { margin-top: 16px; border: 1px solid rgba(127,127,127,.22); border-radius: 12px; padding: 12px 14px; background: rgba(127,127,127,.05); }
.zm-ab-auto-title { font-weight: 600; font-size: 13.5px; } .zm-ab-auto-title { font-weight: 600; font-size: 13.5px; }
.zm-ab-auto-sub { font-size: 12.5px; opacity: .65; margin-top: 2px; line-height: 1.45; } .zm-ab-auto-sub { font-size: 12.5px; opacity: .65; margin-top: 2px; line-height: 1.45; }
@@ -35763,18 +35903,22 @@ html.zm-theme-dark .zm-fk-chip.zm-fk-chip-x { border-color: rgba(255,255,255,.25
.zm-log-wait { color: #e3c04a; font-weight: 600; white-space: pre-wrap; } .zm-log-wait { color: #e3c04a; font-weight: 600; white-space: pre-wrap; }
.zm-si-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; margin-top: 14px; } .zm-si-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; margin-top: 14px; }
.zm-si-group { margin: 22px 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; opacity: .6; } .zm-si-group { margin: 22px 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; opacity: .6; }
.zm-si-cols { column-width: 340px; column-gap: 14px; } .zm-si-mas { display: flex; align-items: flex-start; gap: 14px; }
.zm-si-cols > .zm-si-box { break-inside: avoid; -webkit-column-break-inside: avoid; display: inline-block; width: 100%; margin: 0 0 14px; vertical-align: top; } .zm-si-mas.zm-si-even { align-items: stretch; }
.zm-si-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.zm-si-col > .zm-si-box { margin: 0 !important; }
.zm-si-even .zm-si-col > .zm-si-box { flex: 1 0 auto; }
.zm-si-box { border: 1px solid rgba(0,0,0,.1); border-radius: 12px; padding: 12px 14px; background: rgba(0,0,0,.02); min-width: 0; box-sizing: border-box; } .zm-si-box { border: 1px solid rgba(0,0,0,.1); border-radius: 12px; padding: 12px 14px; background: rgba(0,0,0,.02); min-width: 0; box-sizing: border-box; }
div > .zm-si-box { margin-top: 14px; } div > .zm-si-box { margin-top: 14px; }
.zm-si-grid > .zm-si-box { margin-top: 0; } .zm-si-grid > .zm-si-box { margin-top: 0; }
html.zm-theme-dark .zm-si-box { border-color: rgba(255,255,255,.12); background: rgba(255,255,255,.03); } html.zm-theme-dark .zm-si-box { border-color: rgba(255,255,255,.12); background: rgba(255,255,255,.03); }
.zm-si-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; } .zm-si-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.zm-si-head h4 { margin: 0; font-size: 14px; } .zm-si-head h4 { margin: 0; font-size: 14px; }
.zm-si-box .zm-row { padding: 5px 0; gap: 12px; } .zm-si-box .zm-row { padding: 5px 0; gap: 12px; display: grid; grid-template-columns: minmax(96px, 42%) minmax(0, 1fr); align-items: center; }
.zm-si-box .zm-row > .zm-label { min-width: 150px; } .zm-si-box .zm-row > .zm-label { min-width: 0 !important; width: auto !important; overflow-wrap: anywhere; }
.zm-si-box .zm-row > .zm-badge { justify-self: start; max-width: 100%; white-space: normal; border-radius: 10px; }
.zm-si-val { overflow-wrap: anywhere; } .zm-si-val { overflow-wrap: anywhere; }
@media (max-width: 600px) { .zm-si-box .zm-row > .zm-label { min-width: 110px; } }
.zm-si-pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-size: 12px; max-height: 260px; overflow: auto; } .zm-si-pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-size: 12px; max-height: 260px; overflow: auto; }
.zm-chips { display: flex; flex-wrap: wrap; gap: 6px; } .zm-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.zm-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: rgba(110,118,129,.14); } .zm-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: rgba(110,118,129,.14); }
@@ -36526,6 +36670,66 @@ return view.extend({
return E('div', { 'class': 'zm-si-box' }, [ E('div', { 'class': 'zm-si-head' }, [ E('h4', {}, [ title ]), badge || E([]) ]) ].concat(body)); return E('div', { 'class': 'zm-si-box' }, [ E('div', { 'class': 'zm-si-head' }, [ E('h4', {}, [ title ]), badge || E([]) ]) ].concat(body));
} }
function siSitesSorted() {
return (si.sites || []).map(function(x, i) { return { x: x, i: i }; }).sort(function(a, b) {
return (a.x.ok ? 1 : 0) - (b.x.ok ? 1 : 0) || a.i - b.i;
}).map(function(o) { return o.x; });
}
var SI_COL_MIN = 330, SI_GAP = 14;
function siMasonry(boxes) {
var host = E('div', { 'class': 'zm-si-mas' }), col = E('div', { 'class': 'zm-si-col' });
boxes.forEach(function(b) { col.appendChild(b); });
host.appendChild(col);
host._boxes = boxes; host._w = 0;
var relayout = function() { siMasLayout(host); };
if (window.ResizeObserver) new ResizeObserver(relayout).observe(host);
else { window.addEventListener('resize', relayout); setTimeout(relayout, 0); setTimeout(relayout, 400); }
return host;
}
function siSpread(cols) {
var hi = 0, lo = Infinity;
cols.forEach(function(c) { hi = Math.max(hi, c.h); lo = Math.min(lo, c.h); });
return hi - lo;
}
function siAssign(hs, n, order) {
var cols = [];
for (var i = 0; i < n; i++) cols.push({ h: 0, items: [] });
order.forEach(function(k) {
var best = cols[0];
cols.forEach(function(c) { if (c.h < best.h - 0.5) best = c; });
best.items.push(k); best.h += hs[k] + SI_GAP;
});
cols.forEach(function(c) { c.items.sort(function(a, b) { return a - b; }); });
return cols;
}
function siMasLayout(host) {
var boxes = host._boxes, w = host.clientWidth;
if (!w || !boxes || !host.isConnected) return;
var n = Math.max(1, Math.min(boxes.length, Math.floor((w + SI_GAP) / (SI_COL_MIN + SI_GAP))));
if (w === host._w && n === host._n) return;
host._w = w; host._n = n;
host.classList.remove('zm-si-even');
host.innerHTML = '';
var colW = (w - SI_GAP * (n - 1)) / n;
var meter = E('div', { 'class': 'zm-si-col', 'style': 'flex:0 0 ' + colW + 'px; width:' + colW + 'px' });
boxes.forEach(function(b) { meter.appendChild(b); });
host.appendChild(meter);
var hs = boxes.map(function(b) { return b.offsetHeight; });
host.innerHTML = '';
var idx = boxes.map(function(b, i) { return i; });
var plans = [ siAssign(hs, n, idx), siAssign(hs, n, idx.slice().sort(function(a, b) { return hs[b] - hs[a] || a - b; })) ];
var plan = siSpread(plans[1]) + 1 < siSpread(plans[0]) ? plans[1] : plans[0];
plan.sort(function(a, b) { return (a.items.length ? a.items[0] : 1e9) - (b.items.length ? b.items[0] : 1e9); });
plan.forEach(function(c) {
if (!c.items.length) return;
var col = E('div', { 'class': 'zm-si-col' });
c.items.forEach(function(k) { col.appendChild(boxes[k]); });
host.appendChild(col);
});
if (n > 1) host.classList.add('zm-si-even');
}
function siSections() { function siSections() {
var l = (si.sections || []).slice(), i = -1; var l = (si.sections || []).slice(), i = -1;
l.forEach(function(s, k) { if (s.title === 'Компоненты ZM') i = k; }); l.forEach(function(s, k) { if (s.title === 'Компоненты ZM') i = k; });
@@ -36547,7 +36751,7 @@ return view.extend({
if (si.packages && si.packages.length) out.push('', '== Пакеты ==', si.packages.join(', ')); if (si.packages && si.packages.length) out.push('', '== Пакеты ==', si.packages.join(', '));
if (si.sites && si.sites.length) { if (si.sites && si.sites.length) {
out.push('', '== Доступность сайтов: ' + si.sites_ok + '/' + si.sites_total + ' =='); out.push('', '== Доступность сайтов: ' + si.sites_ok + '/' + si.sites_total + ' ==');
si.sites.forEach(function(x) { out.push((x.ok ? '[ OK ] ' : '[FAIL] ') + x.name); }); siSitesSorted().forEach(function(x) { out.push((x.ok ? '[ OK ] ' : '[FAIL] ') + x.name); });
} }
out.push('', '(адреса, ключи, пароли и ссылки подписок в отчёт не включены)'); out.push('', '(адреса, ключи, пароли и ссылки подписок в отчёт не включены)');
return out.join('\n'); return out.join('\n');
@@ -36576,20 +36780,20 @@ return view.extend({
siBody.innerHTML = ''; siBody.innerHTML = '';
if (!si || siBusy) return; if (!si || siBusy) return;
var SYS = { 'Роутер': 1, 'Память': 1, 'Система': 1, 'Сеть': 1 }; var SYS = { 'Роутер': 1, 'Память': 1, 'Система': 1, 'Сеть': 1 };
var gSys = E('div', { 'class': 'zm-si-cols' }), gZm = E('div', { 'class': 'zm-si-cols' }); var bSys = [], bZm = [];
siSections().forEach(function(s) { siSections().forEach(function(s) {
if (!s.rows || !s.rows.length) return; if (!s.rows || !s.rows.length) return;
(SYS[s.title] ? gSys : gZm).appendChild(siBox(s.title, s.rows.map(function(r) { (SYS[s.title] ? bSys : bZm).push(siBox(s.title, s.rows.map(function(r) {
return E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, [ r[0] ]), siState(r[1], r[2]) ]); return E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, [ r[0] ]), siState(r[1], r[2]) ]);
}))); })));
}); });
if (si.hosts && si.hosts.length) gZm.appendChild(siBox('Домены в hosts', [ E('div', { 'class': 'zm-chips' }, si.hosts.map(function(h) { return E('span', { 'class': 'zm-chip' }, [ h ]); })) ])); if (si.hosts && si.hosts.length) bZm.push(siBox('Домены в hosts', [ E('div', { 'class': 'zm-chips' }, si.hosts.map(function(h) { return E('span', { 'class': 'zm-chip' }, [ h ]); })) ]));
if (gSys.childNodes.length) { siBody.appendChild(E('div', { 'class': 'zm-si-group' }, 'Роутер и сеть')); siBody.appendChild(gSys); } if (bSys.length) { siBody.appendChild(E('div', { 'class': 'zm-si-group' }, 'Роутер и сеть')); siBody.appendChild(siMasonry(bSys)); }
if (gZm.childNodes.length) { siBody.appendChild(E('div', { 'class': 'zm-si-group' }, 'Zapret Manager и компоненты')); siBody.appendChild(gZm); } if (bZm.length) { siBody.appendChild(E('div', { 'class': 'zm-si-group' }, 'Zapret Manager и компоненты')); siBody.appendChild(siMasonry(bZm)); }
if (si.strategy) siBody.appendChild(siBox('Стратегия Zapret', [ E('pre', { 'class': 'zm-si-pre' }, [ si.strategy ]) ])); if (si.strategy) siBody.appendChild(siBox('Стратегия Zapret', [ E('pre', { 'class': 'zm-si-pre' }, [ si.strategy ]) ]));
if (si.sites && si.sites.length) { if (si.sites && si.sites.length) {
var ok = si.sites_ok, all = si.sites_total, cls = ok === all ? 'zm-ok' : ok * 2 >= all ? 'zm-warn' : 'zm-bad'; var ok = si.sites_ok, all = si.sites_total, cls = ok === all ? 'zm-ok' : ok * 2 >= all ? 'zm-warn' : 'zm-bad';
siBody.appendChild(siBox('Доступность сайтов', [ E('div', { 'class': 'zm-chips' }, si.sites.map(function(x) { siBody.appendChild(siBox('Доступность сайтов', [ E('div', { 'class': 'zm-chips' }, siSitesSorted().map(function(x) {
return E('span', { 'class': 'zm-chip ' + (x.ok ? 'zm-chip-ok' : 'zm-chip-bad') }, [ E('span', { 'class': 'zm-dot' }), x.name ]); return E('span', { 'class': 'zm-chip ' + (x.ok ? 'zm-chip-ok' : 'zm-chip-bad') }, [ E('span', { 'class': 'zm-dot' }), x.name ]);
})) ], E('span', { 'class': 'zm-badge ' + cls }, [ E('span', { 'class': 'zm-dot' }), [ ok + ' из ' + all ] ]))); })) ], E('span', { 'class': 'zm-badge ' + cls }, [ E('span', { 'class': 'zm-dot' }), [ ok + ' из ' + all ] ])));
} }
@@ -37148,7 +37352,7 @@ return view.extend({
{ g: 'zm', product: 'Zapret Manager и ByeTube', author: 'StressOzz', url: 'https://github.com/StressOzz' }, { g: 'zm', product: 'Zapret Manager и ByeTube', author: 'StressOzz', url: 'https://github.com/StressOzz' },
{ g: 'block', product: 'zapret, zapret2', author: 'bol-van', url: 'https://github.com/bol-van' }, { g: 'block', product: 'zapret, zapret2', author: 'bol-van', url: 'https://github.com/bol-van' },
{ g: 'block', product: 'zapret-openwrt', author: 'remittor', url: 'https://github.com/remittor/zapret-openwrt' }, { g: 'block', product: 'zapret-openwrt', author: 'remittor', url: 'https://github.com/remittor/zapret-openwrt' },
{ g: 'block', product: 'Zapret2', author: 'routerich', url: 'https://github.com/routerich' }, { g: 'block', product: 'Zapret2, сервер удалённого доступа', author: 'routerich', url: 'https://github.com/routerich' },
{ g: 'block', product: 'стратегии Flowseal', author: 'Flowseal', url: 'https://github.com/Flowseal/zapret-discord-youtube' }, { g: 'block', product: 'стратегии Flowseal', author: 'Flowseal', url: 'https://github.com/Flowseal/zapret-discord-youtube' },
{ g: 'block', product: 'ByeDPI', author: 'hufrea', url: 'https://github.com/hufrea/byedpi' }, { g: 'block', product: 'ByeDPI', author: 'hufrea', url: 'https://github.com/hufrea/byedpi' },
{ g: 'block', product: 'ByeDPI-OpenWrt', author: 'DPITrickster', url: 'https://github.com/DPITrickster/ByeDPI-OpenWrt' }, { g: 'block', product: 'ByeDPI-OpenWrt', author: 'DPITrickster', url: 'https://github.com/DPITrickster/ByeDPI-OpenWrt' },
@@ -37165,12 +37369,16 @@ return view.extend({
{ g: 'vpn', product: 'MagiTrickle', author: 'MagiTrickle', url: 'https://github.com/MagiTrickle/MagiTrickle' }, { g: 'vpn', product: 'MagiTrickle', author: 'MagiTrickle', url: 'https://github.com/MagiTrickle/MagiTrickle' },
{ g: 'vpn', product: 'AmneziaWG', author: 'amnezia-vpn', url: 'https://github.com/amnezia-vpn' }, { g: 'vpn', product: 'AmneziaWG', author: 'amnezia-vpn', url: 'https://github.com/amnezia-vpn' },
{ g: 'vpn', product: 'Tailscale', author: 'Tailscale', url: 'https://github.com/tailscale/tailscale' }, { g: 'vpn', product: 'Tailscale', author: 'Tailscale', url: 'https://github.com/tailscale/tailscale' },
{ g: 'vpn', product: 'openwrt-tailscale (сборка для OpenWrt)', author: 'GuNanOvO', url: 'https://github.com/GuNanOvO/openwrt-tailscale' },
{ g: 'vpn', product: 'luci-app-tailscale-community', author: 'Tokisaki-Galaxy', url: 'https://github.com/Tokisaki-Galaxy/luci-app-tailscale-community' },
{ g: 'vpn', product: 'NaïveProxy (для Forkozz)', author: 'klzgrad', url: 'https://github.com/klzgrad/naiveproxy' },
{ g: 'vpn', product: 'TailSocks (клиент для Android)', author: 'bropines', url: 'https://github.com/bropines/tailsocks' }, { g: 'vpn', product: 'TailSocks (клиент для Android)', author: 'bropines', url: 'https://github.com/bropines/tailsocks' },
{ g: 'vpn', product: 'awg-openwrt (пакеты AmneziaWG)', author: 'Slava-Shchipunov', url: 'https://github.com/Slava-Shchipunov/awg-openwrt' }, { g: 'vpn', product: 'awg-openwrt (пакеты AmneziaWG)', author: 'Slava-Shchipunov', url: 'https://github.com/Slava-Shchipunov/awg-openwrt' },
{ g: 'vpn', product: 'awg-openwrt (сборки)', author: '2Grey', url: 'https://github.com/2Grey/awg-openwrt' }, { g: 'vpn', product: 'awg-openwrt (сборки)', author: '2Grey', url: 'https://github.com/2Grey/awg-openwrt' },
{ g: 'vpn', product: 'warpscout (разведка WARP)', author: 'vernette', url: 'https://github.com/vernette/warpscout' }, { g: 'vpn', product: 'warpscout (разведка WARP)', author: 'vernette', url: 'https://github.com/vernette/warpscout' },
{ g: 'vpn', product: 'base-relay (реле регистрации WARP)', author: 'nellimonix', url: 'https://github.com/nellimonix/base-relay' }, { g: 'vpn', product: 'base-relay (реле регистрации WARP)', author: 'nellimonix', url: 'https://github.com/nellimonix/base-relay' },
{ g: 'vpn', product: 'генератор WARP для Mixomo', author: 'santa-atmo.ru', url: 'https://santa-atmo.ru' }, { g: 'vpn', product: 'генератор WARP для Mixomo', author: 'santa-atmo.ru', url: 'https://santa-atmo.ru' },
{ g: 'tg', product: 'tg-ws-proxy (список доменов Cloudflare)', author: 'Flowseal', url: 'https://github.com/Flowseal/tg-ws-proxy' },
{ g: 'tg', product: 'tg-ws-proxy-go (MTProto)', author: 'spatiumstas', url: 'https://github.com/spatiumstas/tg-ws-proxy-go' }, { g: 'tg', product: 'tg-ws-proxy-go (MTProto)', author: 'spatiumstas', url: 'https://github.com/spatiumstas/tg-ws-proxy-go' },
{ g: 'tg', product: 'tg-ws-proxy-go (SOCKS5)', author: 'd0mhate', url: 'https://github.com/d0mhate/-tg-ws-proxy-Manager-go' }, { g: 'tg', product: 'tg-ws-proxy-go (SOCKS5)', author: 'd0mhate', url: 'https://github.com/d0mhate/-tg-ws-proxy-Manager-go' },
{ g: 'tg', product: 'tg-ws-proxy-rs (Rust)', author: 'valnesfjord', url: 'https://github.com/valnesfjord/tg-ws-proxy-rs' }, { g: 'tg', product: 'tg-ws-proxy-rs (Rust)', author: 'valnesfjord', url: 'https://github.com/valnesfjord/tg-ws-proxy-rs' },
@@ -43506,7 +43714,7 @@ var ROUTES = [
{ id: 'adblock', title: 'Блокировка рекламы', sub: 'Реклама, трекеры и фишинг — для всех устройств в сети', icon: 'adblock', group: 'Сеть', dot: 'adblock' }, { id: 'adblock', title: 'Блокировка рекламы', sub: 'Реклама, трекеры и фишинг — для всех устройств в сети', icon: 'adblock', group: 'Сеть', dot: 'adblock' },
{ id: 'doh', title: 'DNS over HTTPS', sub: 'Шифрованный DNS для всей сети', icon: 'globe', group: 'Сеть', dot: 'doh' }, { id: 'doh', title: 'DNS over HTTPS', sub: 'Шифрованный DNS для всей сети', icon: 'globe', group: 'Сеть', dot: 'doh' },
{ id: 'awg', title: 'AmneziaWG', sub: 'Туннели AmneziaWG и WARP: установка, ключи, интерфейсы', icon: 'anarchy', group: 'Сеть', dot: 'awg' }, { id: 'awg', title: 'AmneziaWG', sub: 'Туннели AmneziaWG и WARP: установка, ключи, интерфейсы', icon: 'anarchy', group: 'Сеть', dot: 'awg' },
{ id: 'tailscale', title: 'Tailscale', sub: 'Телефон в любой сети — через домашний роутер', icon: 'phone', group: 'Сеть', dot: 'ts' }, { id: 'tailscale', title: 'Удалённый доступ', sub: 'Tailscale: телефон в любой сети — через домашний роутер', icon: 'phone', group: 'Сеть', dot: 'ts' },
{ id: 'terminal', title: 'Терминал', sub: 'Командная строка роутера в браузере (ttyd)', icon: 'terminal', group: 'Сеть', dot: 'term' }, { id: 'terminal', title: 'Терминал', sub: 'Командная строка роутера в браузере (ttyd)', icon: 'terminal', group: 'Сеть', dot: 'term' },
{ id: 'system', title: 'Система', sub: 'Параметры роутера, зеркала и обслуживание', icon: 'cpu', group: 'Сервис' }, { id: 'system', title: 'Система', sub: 'Параметры роутера, зеркала и обслуживание', icon: 'cpu', group: 'Сервис' },
{ id: 'monitor', title: 'Монитор ресурсов', sub: 'Процессор, память, температура и процессы', icon: 'activity', group: 'Сервис' } { id: 'monitor', title: 'Монитор ресурсов', sub: 'Процессор, память, температура и процессы', icon: 'activity', group: 'Сервис' }
@@ -43521,10 +43729,7 @@ function currentRoute() {
var root, shell = null, viewEl, titleEl, subEl, navLinks = {}, navDots = {}, deviceEl, verEl, updateEl, memEl; var root, shell = null, viewEl, titleEl, subEl, navLinks = {}, navDots = {}, deviceEl, verEl, updateEl, memEl;
/* Скрытые пункты меню: список на роутере (health.nav_hidden), копия в браузере — чтобы меню var NAV_LOCK = { dashboard: 1 }, NAV_BUILTIN = { dashboard: 1, system: 1, monitor: 1 };
не мигало при загрузке. Дашборд скрыть нельзя. Hosts и блокировка рекламы — встроенные
функции, «неустановленными» не считаются. */
var NAV_LOCK = { dashboard: 1 }, NAV_BUILTIN = { dashboard: 1, hosts: 1, adblock: 1, system: 1, monitor: 1 };
var navHidden = navParse(sget(K_NAV)), navEdit = null, navSaving = false, navGroups = [], navGear = null, navNav = null, navHealth = null; var navHidden = navParse(sget(K_NAV)), navEdit = null, navSaving = false, navGroups = [], navGear = null, navNav = null, navHealth = null;
function navParse(v) { var o = {}; String(v || '').split(/[\s,]+/).forEach(function (k) { if (/^[a-z0-9_]{1,32}$/.test(k) && !NAV_LOCK[k]) o[k] = 1; }); return o; } function navParse(v) { var o = {}; String(v || '').split(/[\s,]+/).forEach(function (k) { if (/^[a-z0-9_]{1,32}$/.test(k) && !NAV_LOCK[k]) o[k] = 1; }); return o; }
function navList(o) { return ROUTES.map(function (r) { return r.id; }).filter(function (k) { return o[k]; }); } function navList(o) { return ROUTES.map(function (r) { return r.id; }).filter(function (k) { return o[k]; }); }
@@ -43569,7 +43774,7 @@ function navUninstalled() {
if (st === 0 && !navEdit[r.id]) { navEdit[r.id] = 1; n++; } if (st === 0 && !navEdit[r.id]) { navEdit[r.id] = 1; n++; }
}); });
navApply(); navApply();
toast(n ? 'Отмечено неустановленных: ' + n + ' — нажмите «Готово»' : (navHealth ? 'Неустановленных разделов нет' : 'Состояние компонентов ещё загружается — попробуйте через пару секунд'), 'info'); toast(n ? 'Отмечено неустановленных и невключённых: ' + n + ' — нажмите «Готово»' : (navHealth ? 'Неустановленных разделов нет' : 'Состояние компонентов ещё загружается — попробуйте через пару секунд'), 'info');
} }
function navAll() { if (navEdit) { navEdit = {}; navApply(); } } function navAll() { if (navEdit) { navEdit = {}; navApply(); } }
function navDone() { function navDone() {
@@ -43696,8 +43901,9 @@ function buildShell() {
nav.appendChild(a); nav.appendChild(a);
}); });
nav.appendChild(E('div', { 'class': 'zmw-nav-editfoot' }, [ nav.appendChild(E('div', { 'class': 'zmw-nav-editfoot' }, [
E('button', { 'class': 'zmw-nav-btn', 'type': 'button', 'click': function () { navUninstalled(); } }, [ 'Скрыть неустановленные' ]), E('button', { 'class': 'zmw-nav-btn zmw-nav-btn-wide', 'type': 'button', 'click': function () { navUninstalled(); } }, [ 'Скрыть неустановленные' ]),
E('button', { 'class': 'zmw-nav-btn', 'type': 'button', 'click': function () { navAll(); } }, [ 'Показать все' ]) E('button', { 'class': 'zmw-nav-btn', 'type': 'button', 'click': function () { navAll(); } }, [ 'Показать все' ]),
E('button', { 'class': 'zmw-nav-btn zmw-nav-btn-done', 'type': 'button', 'click': function () { navDone(); } }, [ 'Готово' ])
])); ]));
navApply(); navApply();
@@ -44516,7 +44722,11 @@ html[data-theme="dark"] .zmw-brand-name b, html[data-theme="depth"] .zmw-brand-n
.zmw-nav-gear.zmw-has > i { display: block; } .zmw-nav-gear.zmw-has > i { display: block; }
.zmw-nav-gear.zmw-on > i { display: none; } .zmw-nav-gear.zmw-on > i { display: none; }
.zmw-nav-editfoot, .zmw-nav-eye, .zmw-nav-lock { display: none; } .zmw-nav-editfoot, .zmw-nav-eye, .zmw-nav-lock { display: none; }
.zmw-nav-editing .zmw-nav-editfoot { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 4px 4px; } .zmw-nav-editing .zmw-nav-editfoot { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; padding: 10px 4px 4px; }
.zmw-nav-editfoot .zmw-nav-btn { min-width: 0; padding: 8px 10px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.zmw-nav-editfoot .zmw-nav-btn-wide { grid-column: 1 / -1; }
.zmw-nav-editfoot .zmw-nav-btn-done { color: #fff; background: var(--grad); border-color: transparent; }
.zmw-nav-editfoot .zmw-nav-btn-done:hover { color: #fff; border-color: transparent; filter: brightness(1.08); }
.zmw-nav-btn { border: 1px solid var(--border); background: var(--surface-2); color: var(--text-2); border-radius: 9px; padding: 6px 10px; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; } .zmw-nav-btn { border: 1px solid var(--border); background: var(--surface-2); color: var(--text-2); border-radius: 9px; padding: 6px 10px; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }
.zmw-nav-btn:hover { color: var(--text); border-color: var(--a1); } .zmw-nav-btn:hover { color: var(--text); border-color: var(--a1); }
.zmw-nav-editing .zmw-nav-eye { .zmw-nav-editing .zmw-nav-eye {
@@ -45984,7 +46194,7 @@ html[data-theme="oled"] .zmw-nav-item { border-radius: 999px; }
html[data-theme="oled"] .zmw-nav-item.zmw-active { background: #ffffff; color: #000000; } html[data-theme="oled"] .zmw-nav-item.zmw-active { background: #ffffff; color: #000000; }
html[data-theme="oled"] .zmw-nav-item.zmw-active::before { display: none; } html[data-theme="oled"] .zmw-nav-item.zmw-active::before { display: none; }
html[data-theme="oled"] .zmw-nav-item.zmw-active .zmw-nav-ico { background: #000000; color: #ffffff; border-color: #000000; } html[data-theme="oled"] .zmw-nav-item.zmw-active .zmw-nav-ico { background: #000000; color: #ffffff; border-color: #000000; }
html[data-theme="oled"] .zmw-nav-gear.zmw-on { background: #ffffff; color: #000000; border-color: #ffffff; } html[data-theme="oled"] .zmw-nav-gear.zmw-on, html[data-theme="oled"] .zmw-nav-editfoot .zmw-nav-btn-done { background: #ffffff; color: #000000; border-color: #ffffff; }
html[data-theme="oled"] .zmw-nav-editing .zmw-nav-eye { color: #ffffff; background: rgba(255,255,255,.12); } html[data-theme="oled"] .zmw-nav-editing .zmw-nav-eye { color: #ffffff; background: rgba(255,255,255,.12); }
html[data-theme="oled"] .zmw-nav-gear > i { background: #ffffff; } html[data-theme="oled"] .zmw-nav-gear > i { background: #ffffff; }
html[data-theme="oled"] #zmw-view .zm-card { border-radius: 20px; } html[data-theme="oled"] #zmw-view .zm-card { border-radius: 20px; }