diff --git a/ZapretManager_LuCI.sh b/ZapretManager_LuCI.sh index b734bb3..adb4bcf 100644 --- a/ZapretManager_LuCI.sh +++ b/ZapretManager_LuCI.sh @@ -1,10 +1,10 @@ #!/bin/sh -# Version: 2.41 +# Version: 2.43 set -e GREEN="\033[1;32m"; CYAN="\033[1;36m"; YELLOW="\033[1;33m"; MAGENTA="\033[1;35m"; BLUE="\033[0;34m"; NC="\033[0m"; DGRAY="\033[38;5;244m" -ZM_NEW_VER="2.41" +ZM_NEW_VER="2.43" _zmi_say() { echo -e "${CYAN}==>${NC} $*"; } _zmi_ok() { echo -e " ${GREEN}✓${NC} $*"; } _zmi_step() { echo -e " → $*"; } @@ -79,7 +79,7 @@ cat > '/opt/zapret-manager-luci/backend.sh.zm-new' << 'ZM_INSTALLER_EOF' umask 022 CONF="/etc/config/zapret" -ZM_VERSION="2.41" +ZM_VERSION="2.43" ZM_SCRIPT_URL="https://raw.githubusercontent.com/StressOzz/Zapret-Manager/refs/heads/main/ZapretManager_LuCI.sh" GH_RAW="https://raw.githubusercontent.com" GH_MAIN="https://github.com" @@ -1241,6 +1241,11 @@ zapret_action() { update) job_start install_zapret do_install_zapret ;; remove) job_start remove_zapret do_remove_zapret ;; start) /etc/init.d/zapret start >/dev/null 2>&1; zapret_restart; status ;; + restart) + [ -x /etc/init.d/zapret ] || { echo '{"error":"Zapret не установлен"}'; return 1; } + zapret_restart + for p in 1 2 3 4 5; do pgrep -f "/opt/zapret/" >/dev/null 2>&1 && break; sleep 1; done + status ;; stop) /etc/init.d/zapret stop >/dev/null 2>&1 for p in $(pgrep -f "/opt/zapret/" 2>/dev/null); do kill -9 "$p" 2>/dev/null; done @@ -1350,6 +1355,11 @@ zapret2_action() { remove) job_start remove_zapret2 do_remove_zapret2 ;; start) /etc/init.d/zapret2 start >/dev/null 2>&1; status ;; stop) /etc/init.d/zapret2 stop >/dev/null 2>&1; status ;; + restart) + [ -x /etc/init.d/zapret2 ] || { echo '{"error":"Zapret2 не установлен"}'; return 1; } + /etc/init.d/zapret2 restart >/dev/null 2>&1 + for p in 1 2 3 4 5; do /etc/init.d/zapret2 status >/dev/null 2>&1 && break; sleep 1; done + status ;; *) echo '{"error":"неизвестное действие"}' ;; esac } @@ -2099,6 +2109,30 @@ _hosts_block() { "199.232.210.248 scdn.co i.scdn.co line-up.scdn.co mosaic.scdn.co daily-mix.scdn.co lineup-images.scdn.co encore.scdn.co image-cdn-fa.scdn.co accounts.scdn.co www.scdn.co www-growth.scdn.co av.scdn.co seafoam.scdn.co" \ "23.48.23.145 heads-ak-spotify-com.akamaized.net" \ "45.155.204.190 xpui.app.spotify.com" ;; + roblox) printf '%s\n' \ + "#tr.rbxcdn.com" \ + "108.156.22.8 tr.rbxcdn.com" \ + "108.157.32.114 tr.rbxcdn.com" \ + "18.65.147.108 tr.rbxcdn.com" \ + "18.65.147.112 tr.rbxcdn.com" \ + "13.224.181.18 tr.rbxcdn.com" \ + "13.224.181.74 tr.rbxcdn.com" \ + "54.230.253.22 tr.rbxcdn.com" \ + "54.230.253.81 tr.rbxcdn.com" \ + "54.230.253.48 tr.rbxcdn.com" \ + "54.230.253.59 tr.rbxcdn.com" \ + "143.204.214.34 tr.rbxcdn.com" \ + "143.204.214.67 tr.rbxcdn.com" \ + "143.204.214.92 tr.rbxcdn.com" \ + "99.84.181.25 tr.rbxcdn.com" \ + "99.84.181.63 tr.rbxcdn.com" \ + "65.8.158.45 tr.rbxcdn.com" \ + "65.8.158.112 tr.rbxcdn.com" ;; + riot) printf '%s\n' \ + "#Riotgames" \ + "159.194.200.33 auth.riotgames.com authenticate.riotgames.com entitlements.auth.riotgames.com clientconfig.rpg.riotgames.com" \ + "159.194.200.33 playerpreferences.riotgames.com login.riotgames.com riotgames.com www.riotgames.com account.riotgames.com" \ + "159.194.200.33 recovery.riotgames.com riot-client.riotgames.com playvalorant.com leagueoflegends.com" ;; rutracker) printf '%s\n' \ "#Rutracker" \ "172.67.182.196 rutracker.org" ;; @@ -2134,7 +2168,7 @@ _hosts_has_line() { } hosts_status() { - local blocks="nalog ntc instagram librusec ai twitch telegram spotify spotifyext rutor rutracker scell githubraw github tapeop finland" b first=1 + local blocks="nalog ntc instagram librusec ai twitch telegram spotify spotifyext rutor rutracker scell roblox riot githubraw github tapeop finland" b first=1 local geohide="" if grep -q '^### geohide.ru: hosts file' "$HOSTS_FILE" 2>/dev/null; then if grep -q '^# Регион серверов: US$' "$HOSTS_FILE" 2>/dev/null; then geohide="us" @@ -2520,6 +2554,151 @@ system_toggle_flow_offloading_fix() { } +ZM_FLASH_PATHS="/opt/zapret-manager-luci /usr/libexec/rpcd/zapret-manager /usr/share/rpcd/acl.d/luci-app-zapret-manager.json /usr/share/luci/menu.d/luci-app-zapret-manager.json /www/luci-static/resources/view/zapret-manager /www/luci-static/resources/zapret-manager /www/luci-static/resources/bytetube /www/luci-static/resources/view/bytetube /www/zm /www/zm-webui.html /usr/share/zm-redbtn" + +_zm_kb() { + [ $# -gt 0 ] || { echo 0; return 0; } + du -sk "$@" 2>/dev/null | awk '{ s += $1 } END { print s + 0 }' +} + +_zm_aged() { + [ -e "$1" ] || [ -L "$1" ] || return 1 + [ "${2:-0}" -gt 0 ] || return 0 + [ -n "$(find "$1" -maxdepth 0 -mmin +"$2" 2>/dev/null)" ] +} + +_zm_junk() { + local age=0 f n d + for f in /tmp/zm-run.* /tmp/zm-wait.* /tmp/zm-quiet.* /tmp/zm-pkg.* /tmp/ytb-ct.* /tmp/ytb-diag.* /tmp/ytb-ins.* /tmp/ytb-dl /tmp/zm-zash /tmp/netshift-sbext.* \ + /tmp/mihomo.gz /tmp/zashboard.zip /tmp/zashboard /tmp/metacubexd.tgz /tmp/metacubexd /tmp/tg-ws-proxy.ipk /tmp/tg-ws-proxy.apk /tmp/zm_uninstall_panel.sh \ + "$JOBS_DIR"/install_tmp "$JOBS_DIR"/install_z2_tmp "$JOBS_DIR"/flowseal.zip "$JOBS_DIR"/flowseal_src "$JOBS_DIR"/tg-ws-proxy-rs.tar.gz \ + "$JOBS_DIR"/netshift-src "$JOBS_DIR"/forkop-src "$JOBS_DIR"/forkop-import "$JOBS_DIR"/zm_head.* "$JOBS_DIR"/steer-list.* "$JOBS_DIR"/tgws_install.* \ + "$JOBS_DIR"/geohide_hosts.tmp "$JOBS_DIR"/cleanup.* "$ST_RUN"/*.src "$ST_RUN"/cat.flat \ + /opt/zapret-manager-luci/*.zm-new /www/luci-static/resources/zapret-manager/*.zm-new "$ZM_STATE_DIR"/*.tmp "$ST_DIR"/*.tmp "$ST_DIR"/lists/*.tmp \ + "$ST_USER_DIR"/*.tmp "$ST_USER_DIR"/*.tmp.* "$HOSTS_FILE.zmtmp" "$HOSTS_FILE.zmdrop"; do + [ "$f" = "$JOBS_DIR/cleanup.$$" ] && continue + _zm_aged "$f" "$age" && echo "$f" + done + [ "$1" = install ] || { [ -e /tmp/zm_update_install.log ] && echo /tmp/zm_update_install.log; } + [ -e "$ZM_UP_FILE" ] && echo "$ZM_UP_FILE" + for f in /tmp/zm-rpc.*; do _zm_aged "$f" 10 && echo "$f"; done + for f in "$JOBS_DIR"/*.pid; do + [ -f "$f" ] || continue + n="$(basename "$f" .pid)" + _job_running "$n" || [ -f "$JOBS_DIR/$n.log" ] || echo "$f" + done + for f in "$JOBS_DIR"/install_z* "$JOBS_DIR"/sysinfo.json "$JOBS_DIR"/sysinfo.res "$JOBS_DIR"/sysinfo.urls "$JOBS_DIR"/tzdet.* "$JOBS_DIR"/excl_arp_tmp \ + "$JOBS_DIR"/yv_* "$JOBS_DIR"/nfq_drop_old "$JOBS_DIR"/nfq_drop_new "$JOBS_DIR"/steer.hide.in "$JOBS_DIR"/mixomo.hide.in "$JOBS_DIR"/forkop-svc.out "$JOBS_DIR"/forkop-lists.out \ + "$ST_RUN"/dl.* "$ST_RUN"/used.* "$ST_RUN"/srs.*.done "$ST_RUN"/lists.json "$ZM_STATE_DIR"/latest.* "$ZM_STATE_DIR"/versions.json "$ZM_STATE_DIR"/steer.latest; do + case "$f" in "$JOBS_DIR"/sysinfo.*) _job_running sysinfo && continue ;; esac + [ -e "$f" ] && echo "$f" + done + for f in /opt/zapret-manager-luci/forkop.uc /usr/lib/zapret-manager* /etc/zapret_manager_expert_mode* /usr/share/zm-redbtn/lists \ + /www/luci-static/resources/view/zapret-manager/test.js /www/luci-static/resources/view/zapret-manager/redbtn.js \ + /www/luci-static/resources/view/zapret-manager/youtube.js /www/luci-static/resources/view/zapret-manager/game.js \ + /www/luci-static/resources/view/zapret-manager/discord.js /www/luci-static/resources/view/zapret-manager/exclusions.js \ + /www/luci-static/resources/view/zapret-manager/forkop.js; do + [ -e "$f" ] || [ -L "$f" ] && echo "$f" + done + for f in "$JOBS_DIR"/*.log; do + [ -f "$f" ] || continue + n="$(basename "$f" .log)" + _job_running "$n" && continue + echo "$f" + [ -f "$JOBS_DIR/$n.pid" ] && echo "$JOBS_DIR/$n.pid" + done + if [ -d "$ZM_LISTS_DIR" ]; then + for f in "$ZM_LISTS_DIR"/.*; do + [ -f "$f" ] || continue + echo "$f" + done + if [ -s "$RB_SHARE/services.conf" ]; then + d=" $(grep -v '^#' "$RB_SHARE/services.conf" | cut -d'|' -f3,4 | tr ',|\n' ' ') " + for f in "$ZM_LISTS_DIR"/*.lst; do + [ -f "$f" ] || continue + case "$d" in *" ${f##*/} "*) ;; *) echo "$f" ;; esac + done + fi + fi + if [ -d "$ST_DIR/lists" ] && [ -s "$RB_SHARE/services.conf" ] && ! _st_running; then + ls "$ST_DIR/lists" 2>/dev/null | awk -F'|' -v sel="$ST_SEL" -v conf="$RB_SHARE/services.conf" -v rem="$ST_CAT_SVC" -v dir="$ST_DIR/lists" ' + function load(file, l, a, c, m, i) { while ((getline l < file) > 0) { if (l ~ /^#/) continue; split(l, a, "|"); if (!(a[1] in on)) continue; m = split(a[8], c, ","); for (i = 1; i <= m; i++) if (c[i] != "") keep[c[i]] = 1 } close(file) } + BEGIN { while ((getline l < sel) > 0) if (l != "") { on[l] = 1; keep[l] = 1 } close(sel); load(conf); load(rem) } + { n = $0; sub(/\..*$/, "", n); if (n == "" || (n in keep)) next; print dir "/" $0 }' | while IFS= read -r f; do + grep -qF "$f" "$STL_SPEC" "$STL_YAML" 2>/dev/null || echo "$f" + done + fi + return 0 +} + +_zm_mem_kb() { awk '/^MemAvailable:/ { print $2; f = 1; exit } /^MemFree:/ { m = $2 } END { if (!f) print m + 0 }' /proc/meminfo 2>/dev/null; } +_zm_df_kb() { df -kP "$1" 2>/dev/null | awk 'NR == 2 { print $4 + 0; f = 1 } END { if (!f) print 0 }'; } + +_zm_usage_json() { + local list="$JOBS_DIR/cleanup.$$" jk=0 jn=0 + mkdir -p "$JOBS_DIR" + _zm_junk "$1" > "$list" 2>/dev/null + if [ -s "$list" ]; then + jn="$(grep -c . "$list")" + jk="$(_zm_kb $(cat "$list"))" + fi + rm -f "$list" + printf '"flash_kb":%s,"lists_kb":%s,"tmp_kb":%s,"junk_kb":%s,"junk_n":%s,"flash_free_kb":%s,"tmp_free_kb":%s,"mem_free_kb":%s' \ + "$(_zm_kb $ZM_FLASH_PATHS)" "$(_zm_kb "$ST_DIR/lists" "$ZM_LISTS_DIR")" "$(_zm_kb "$JOBS_DIR")" "${jk:-0}" "${jn:-0}" \ + "$(_zm_df_kb /opt/zapret-manager-luci)" "$(_zm_df_kb /tmp)" "$(_zm_mem_kb)" +} + +zm_cleanup_status() { + printf '{%s}\n' "$(_zm_usage_json)" +} + +zm_cleanup() { + local mode="$1" j f k n=0 tk=0 fk=0 m0 m1 list="$JOBS_DIR/cleanup.$$" + if [ "$mode" != install ]; then + if j="$(_zm_busy_job)"; then + printf '{"error":"%s"}\n' "$(esc "идёт операция «$j» — дождитесь её окончания и запустите очистку")" + return 1 + fi + if [ -f /tmp/zm_update_install.sh ]; then + echo '{"error":"идёт обновление панели — дождитесь его окончания"}' + return 1 + fi + fi + mkdir -p "$JOBS_DIR" + m0="$(_zm_mem_kb)" + _zm_junk "$mode" > "$list" 2>/dev/null + while IFS= read -r f; do + case "$f" in + /tmp/zm_update_install.sh) continue ;; + /tmp/zm[-_]*|/tmp/ytb-*|/tmp/netshift-sbext.*|/tmp/mihomo.gz|/tmp/zashboard|/tmp/zashboard.zip|/tmp/metacubexd|/tmp/metacubexd.tgz|/tmp/tg-ws-proxy.ipk|/tmp/tg-ws-proxy.apk) ;; + /tmp/zapret-manager-luci/?*|/opt/zapret-manager-luci/?*|/etc/zm-steer/?*|/etc/hosts.zmtmp|/etc/hosts.zmdrop) ;; + /usr/lib/zapret-manager*|/etc/zapret_manager_expert_mode*|/usr/share/zm-redbtn/lists|/www/luci-static/resources/zapret-manager/?*|/www/luci-static/resources/view/zapret-manager/?*) ;; + *) continue ;; + esac + case "$f" in */../*|*/..) continue ;; esac + [ -e "$f" ] || [ -L "$f" ] || continue + k="$(_zm_kb "$f")" + rm -rf "$f" 2>/dev/null + [ -e "$f" ] && continue + n=$((n + 1)) + case "$f" in /tmp/*) tk=$((tk + k)) ;; *) fk=$((fk + k)) ;; esac + done < "$list" + rm -f "$list" + for f in "$ZM_STATE_DIR"/*.lock; do + [ -d "$f" ] && _zm_aged "$f" 5 && rmdir "$f" 2>/dev/null + done + _st_running || rm -f "$ST_RUN"/dl.* "$ST_RUN"/used.* "$ST_RUN"/srs.*.done 2>/dev/null + rmdir "$ST_DIR/lists" "$ZM_LISTS_DIR" 2>/dev/null + rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null + if [ "$mode" != install ]; then + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null + fi + m1="$(_zm_mem_kb)" + k=$(( ${m1:-0} - ${m0:-0} )); [ "$k" -gt 0 ] || k=0 + printf '{"ok":true,"removed":%s,"freed_flash_kb":%s,"freed_tmp_kb":%s,"freed_mem_kb":%s,%s}\n' "$n" "$fk" "$tk" "$k" "$(_zm_usage_json "$mode")" +} + system_uninstall_panel() { local script="/tmp/zm_uninstall_panel.sh" j if j="$(_zm_busy_job)"; then @@ -2537,6 +2716,8 @@ ZM_PATHS="/opt/zapret-manager-luci /usr/libexec/rpcd/zapret-manager \ /www/luci-static/resources/bytetube \ /www/luci-static/resources/view/bytetube \ /tmp/zapret-manager-luci /tmp/luci-indexcache* /tmp/luci-modulecache/* \ + /tmp/zm-run.* /tmp/zm-wait.* /tmp/zm-quiet.* /tmp/zm-pkg.* /tmp/zm-rpc.* /tmp/zm_update_install.log \ + /usr/lib/zapret-manager* /etc/zapret_manager_expert_mode* /etc/hosts.zmtmp /etc/hosts.zmdrop \ /www/zm /www/zm-webui.html /etc/zm-warp-own.conf" zm_try=0 while :; do @@ -5609,7 +5790,23 @@ health() { local fw=false v6=false [ "$(uci -q get firewall.@defaults[0].flow_offloading)" = 1 ] && [ -f /usr/share/firewall4/templates/ruleset.uc ] && [ "$(_flow_offloading_fix_applied)" = false ] && fw=true [ -f "$CONF" ] && [ "$(_ipv6_enabled_in_zapret)" = false ] && ip -6 route show default 2>/dev/null | grep -q . && v6=true - printf '{"zapret":%s,"zapret2":%s,"bytetube":%s,"tg":%s,"mixomo":%s,"doh":%s,"hosts":%s,"steer":%s,"steer_off":%s,"steer_exit":"%s","awg":%s,"forkop":%s,"flow_warn":%s,"ipv6_warn":%s,"reboot_hint":"%s"}\n' \ + local d_zr="$zr" d_zr2="$zr2" d_bt="$bt" d_tg="$tg" d_mx="$mx" d_doh="$doh" d_sr="$sr" d_fk d_awg d_term=0 awgj + [ "$zr" = 2 ] && d_zr=4 + [ "$zr2" = 2 ] && d_zr2=4 + if [ "$bt" = 2 ]; then case "$(/usr/bin/bytetube status 2>/dev/null)" in *'"enabled":true'*) ;; *) d_bt=4 ;; esac; fi + [ "$tg" = 2 ] && ! ls /etc/rc.d/S*tg-ws-proxy* /etc/rc.d/S*tgws >/dev/null 2>&1 && d_tg=4 + [ "$mx" = 2 ] && ! ls /etc/rc.d/S*mihomo >/dev/null 2>&1 && d_mx=4 + if [ "$doh" = 2 ]; then { [ -f "$DOH_OFF_FLAG" ] || ! ls /etc/rc.d/S*https-dns-proxy >/dev/null 2>&1; } && d_doh=4; fi + [ "$sr" = 5 ] && d_sr=4 + d_fk="$(_fk_health)"; [ "$d_fk" = 5 ] && d_fk=4 + awgj="$(_awg_health)"; d_awg="${awgj%%,*}" + case "$awgj" in *'"awg_total":0') [ "$d_awg" = 1 ] && d_awg=4; [ "$d_awg" = 3 ] && d_awg=2 ;; esac + if _term_installed; then + if _term_running; then d_term=1; elif _term_enabled; then d_term=2; else d_term=4; fi + fi + printf '{"dots":{"zapret":%s,"zapret2":%s,"bytetube":%s,"tg":%s,"mixomo":%s,"doh":%s,"hosts":%s,"steer":%s,"forkop":%s,"awg":%s,"term":%s},' \ + "$d_zr" "$d_zr2" "$d_bt" "$d_tg" "$d_mx" "$d_doh" "$hs" "$d_sr" "${d_fk:-0}" "${d_awg:-0}" "$d_term" + printf '"zapret":%s,"zapret2":%s,"bytetube":%s,"tg":%s,"mixomo":%s,"doh":%s,"hosts":%s,"steer":%s,"steer_off":%s,"steer_exit":"%s","awg":%s,"forkop":%s,"flow_warn":%s,"ipv6_warn":%s,"reboot_hint":"%s"}\n' \ "$zr" "$zr2" "$bt" "$tg" "$mx" "$doh" "$hs" "$sr" \ "$([ -f /etc/zm-steer/stopped ] && echo true || echo false)" "$sx" "$(_awg_health)" "$(_fk_health)" "$fw" "$v6" "$(esc "$(_zm_reboot_hint_get)")" } @@ -7492,7 +7689,7 @@ _rb_list_get() { _rb_say() { echo "==> $*"; } _rb_warn() { echo "!! $*"; } -_rb_svc_src() { grep -hv '^#' "$RB_SHARE/services.conf" 2>/dev/null; [ -f "$ST_CAT_OFF" ] || awk -F'|' -v skip="$ST_CAT_SKIP" '$9 != "" && $9 != "Свои списки каталога" { k = $1; sub(/^c_/, "", k); if (!index(skip, " " k " ")) print }' "$ST_CAT_SVC" 2>/dev/null; } +_rb_svc_src() { grep -hv '^#' "$RB_SHARE/services.conf" 2>/dev/null; [ -f "$ST_CAT_OFF" ] || awk -F'|' -v skip="$ST_CAT_SKIP" -v conf="$RB_SHARE/services.conf" 'BEGIN { while ((getline l < conf) > 0) { if (l ~ /^#/) continue; split(l, a, "|"); if (a[1] == "") continue; b[a[1]] = 1; m = split(a[8], c, ","); for (i = 1; i <= m; i++) if (c[i] != "") b[c[i]] = 1 } } $9 != "" && $9 != "Свои списки каталога" { k = $1; sub(/^c_/, "", k); if (index(skip, " " k " ")) next; if (k ~ /^itdoginfo_/ && (substr(k, 11) in b)) next; print }' "$ST_CAT_SVC" 2>/dev/null; } _rb_svc_field() { _rb_svc_src | grep "^$1|" | head -n1 | cut -d'|' -f"$2"; } _rb_svc_names() { local id; for id in $1; do printf '%s, ' "$(_rb_svc_field "$id" 2)"; done | sed 's/, $//'; } _rb_routable() { [ "$1" = custom ] && [ -n "$(_rb_svc_field custom 1)" ] && return 0; [ -n "$(_rb_svc_field "$1" 3)$(_rb_svc_field "$1" 4)$(_rb_svc_field "$1" 8)" ]; } @@ -8242,8 +8439,9 @@ _st_sel_remap() { local f for f in "$ST_SEL" "$ST_SKIP"; do grep -q '^c_itdoginfo_' "$f" 2>/dev/null || continue - awk -v conf="$RB_SHARE/services.conf" 'BEGIN { while ((getline l < conf) > 0) { split(l, a, "|"); if (a[1] != "" && a[1] !~ /^#/) b[a[1]] = 1 } } - { k = $0; if (k ~ /^c_itdoginfo_/ && (substr(k, 13) in b)) k = substr(k, 13); if (k != "" && !(k in s)) { s[k] = 1; print k } }' "$f" > "$f.tmp" && mv -f "$f.tmp" "$f" + awk -v conf="$RB_SHARE/services.conf" -v ex="$([ "$f" = "$ST_SEL" ] && echo 1)" 'BEGIN { while ((getline l < conf) > 0) { split(l, a, "|"); if (a[1] != "" && a[1] !~ /^#/) { b[a[1]] = 1; m = split(a[8], c, ","); for (i = 1; i <= m; i++) if (c[i] != "") bs[c[i]] = bs[c[i]] " " a[1] } } } + function put(k) { if (k != "" && !(k in s)) { s[k] = 1; print k } } + { k = $0; if (k ~ /^c_itdoginfo_/) { x = substr(k, 13); if (x in b) { put(x); next } if (ex && (x in bs)) { m = split(bs[x], q, " "); for (i = 1; i <= m; i++) put(q[i]); next } } put(k) }' "$f" > "$f.tmp" && mv -f "$f.tmp" "$f" done } _st_migrate @@ -9252,7 +9450,7 @@ _st_cat_build() { grp = clean(f[e, "source_name"]) if (grp == "" && fo[e]) grp = (tolower(f[e, "id"] " " f[e, "source"]) ~ /b4geoip/) ? "b4geoip" : "itdoginfo (allow-domains)" if (grp == "") return - if (!fo[e] && f[e, "source"] == "itdoginfo/allow-domains") { + if (tolower(f[e, "id"] " " f[e, "source"]) ~ /itdoginfo/) { u = f[e, "url"]; b = u; sub(/.*\//, "", b); sub(/\.srs$/, "", b) if (index(bsets, " " b " ") || index(bnames, "|" tolower(nm) "|")) return } @@ -10414,11 +10612,6 @@ steer_list_set() { [ "$n" -gt 0 ] || { rm -f "$tmp" "$tmp.pfx"; echo '{"error":"в списке нет ни одного домена или IP-адреса"}'; return 1; } if [ -s "$tmp" ]; then mv "$tmp" "$f"; else rm -f "$tmp" "$f"; fi if [ -s "$tmp.pfx" ]; then mv "$tmp.pfx" "$ST_USER_DIR/$id.pfx"; else rm -f "$tmp.pfx" "$ST_USER_DIR/$id.pfx"; fi - if [ "$id" = custom ]; then - mkdir -p "$ST_DIR" - [ -f "$ST_SKIP" ] && sed -i '/^custom$/d' "$ST_SKIP" - _rb_in custom "$ST_SEL" || echo custom >> "$ST_SEL" - fi _st_list_changed "$id" "$n" } @@ -14183,6 +14376,8 @@ case "$cmd" in system_toggle_ipv6) system_toggle_ipv6 ;; system_toggle_flow_offloading_fix) system_toggle_flow_offloading_fix ;; system_uninstall_panel) system_uninstall_panel ;; + zm_cleanup_status) zm_cleanup_status ;; + zm_cleanup) zm_cleanup "$1" ;; sysinfo_run) sysinfo_run ;; sysinfo_get) sysinfo_get ;; mirror_status) mirror_status ;; @@ -15089,7 +15284,7 @@ function first_addr(v) { /* Сеть роутера: логические сети (lan, wan…) и устройства (br-lan, eth0, pppoe-wan…). */ function net_info(c) { - let nets = [], devnet = {}; + let nets = [], devnet = {}, netproto = {}; try { c.load("network"); } catch (e) {} try { c.foreach("network", "interface", (x) => { @@ -15098,6 +15293,7 @@ function net_info(c) { if (proto == "pppoe" || proto == "pppoa") dev = proto + "-" + n; if (match(dev, /^@/)) dev = ""; if (dev != "" && !devnet[dev]) devnet[dev] = n; + netproto[n] = proto; push(nets, { name: n, proto, dev }); }); } catch (e) {} @@ -15114,7 +15310,8 @@ function net_info(c) { : t == "1" ? "eth" : ""; if (kind == "") continue; let st = trim(s(fs.readfile(p + "/operstate"))); - push(devs, { name: n, kind, up: st != "down", net: devnet[n] || "" }); + let dn = devnet[n] || (netproto[n] != null ? n : ""); + push(devs, { name: n, kind, up: st != "down", net: dn, proto: dn != "" ? s(netproto[dn]) : "" }); } devs = sort(devs, (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); return { lan_ip: lan, nets, devs }; @@ -15465,7 +15662,7 @@ function cmd_set() { write_sec_ext(c, sec, mode, d.x); set_lists(c, sec, { s: rsets, r: rsubs, l: lists }, services, domains, subnets); - if (own_on) { + if (own_on || !(length(keep.domains) || length(keep.subnets) || length(keep.lists))) { del_keys(c, sec, [ "zm_own_off", "zm_own_domains", "zm_own_subnets", "zm_own_lists" ]); } else { c.set(CFG, sec, "zm_own_off", "1"); @@ -16753,6 +16950,8 @@ list_methods() { json_add_object "system_toggle_ipv6"; json_close_object json_add_object "system_toggle_flow_offloading_fix"; json_close_object json_add_object "system_uninstall_panel"; json_close_object + json_add_object "zm_cleanup_status"; json_close_object + json_add_object "zm_cleanup"; json_close_object json_add_object "sysinfo_run"; json_close_object json_add_object "sysinfo_get"; json_close_object json_add_object "mirror_status"; json_close_object @@ -16860,6 +17059,8 @@ call_method() { system_toggle_ipv6) "$BACKEND" system_toggle_ipv6 ;; system_toggle_flow_offloading_fix) "$BACKEND" system_toggle_flow_offloading_fix ;; system_uninstall_panel) "$BACKEND" system_uninstall_panel ;; + zm_cleanup_status) "$BACKEND" zm_cleanup_status ;; + zm_cleanup) "$BACKEND" zm_cleanup ;; sysinfo_run) "$BACKEND" sysinfo_run ;; sysinfo_get) "$BACKEND" sysinfo_get ;; mirror_status) "$BACKEND" mirror_status ;; @@ -16985,6 +17186,7 @@ cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF' "test_status", "test_results", "zm_update_status", + "zm_cleanup_status", "mixomo_status", "mixomo_proxies", "mixomo_config_get", @@ -17046,6 +17248,7 @@ cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF' "system_toggle_ipv6", "system_toggle_flow_offloading_fix", "system_uninstall_panel", + "zm_cleanup", "mirror_set", "exclusions_toggle", "exclusions_clear", @@ -17236,6 +17439,8 @@ var callSystemToggleQuic = zmDeclare({ object: 'zapret-manager', method: 'system var callSystemToggleIpv6 = zmDeclare({ object: 'zapret-manager', method: 'system_toggle_ipv6', expect: {} }); var callSystemToggleFlowOffloadingFix = zmDeclare({ object: 'zapret-manager', method: 'system_toggle_flow_offloading_fix', expect: {} }); var callSystemUninstallPanel = zmDeclare({ object: 'zapret-manager', method: 'system_uninstall_panel', expect: {} }); +var callZmCleanupStatus = zmDeclare({ object: 'zapret-manager', method: 'zm_cleanup_status', expect: {} }); +var callZmCleanup = zmDeclare({ object: 'zapret-manager', method: 'zm_cleanup', expect: {} }); var callSysinfoRun = zmDeclare({ object: 'zapret-manager', method: 'sysinfo_run', expect: {} }); var callSysinfoGet = zmDeclare({ object: 'zapret-manager', method: 'sysinfo_get', expect: {} }); var callMirrorStatus = zmDeclare({ object: 'zapret-manager', method: 'mirror_status', expect: {} }); @@ -18618,6 +18823,8 @@ return baseclass.extend({ systemToggleIpv6: callSystemToggleIpv6, systemToggleFlowOffloadingFix: callSystemToggleFlowOffloadingFix, systemUninstallPanel: callSystemUninstallPanel, + zmCleanupStatus: callZmCleanupStatus, + zmCleanup: callZmCleanup, sysinfoRun: callSysinfoRun, sysinfoGet: callSysinfoGet, mirrorStatus: callMirrorStatus, @@ -19773,7 +19980,7 @@ return view.extend({ var wrap = E('div', { 'class': 'zm-wrap' }); var card = E('div', { 'class': 'zm-card' }); var headEl = E('div', {}); - var viewEl = E('div', { 'style': 'display:none; margin:10px 0' }); + var viewEl = E('div', { 'style': 'display:none; margin:0 0 14px' }); var actEl = E('div', {}); var logEl = E('pre', { 'class': 'zm-log' }); var busy = false, frame = null, autoTried = false; @@ -19785,6 +19992,22 @@ return view.extend({ function addr() { return 'http://' + window.location.hostname + ':' + (data.port || 7681) + '/'; } + function termTitle() { return 'ttyd · ' + window.location.hostname + ':' + (data.port || 7681); } + + function frameUrl() { + var base = addr(), bg = ''; + if (window.zmTermUrl && String(window.zmTermUrl).indexOf(base) === 0) return window.zmTermUrl; + try { bg = getComputedStyle(document.documentElement).getPropertyValue('--console').trim(); } catch (e) { bg = ''; } + if (!/^#[0-9a-f]{6}$/i.test(bg)) bg = '#0d1117'; + var theme = { + background: bg, foreground: '#e6edf3', cursor: '#e6edf3', + black: '#000000', red: '#d81e00', green: '#5ea702', yellow: '#cfae00', blue: '#427ab3', magenta: '#89658e', cyan: '#00a7aa', white: '#dbded8', + brightBlack: '#686a66', brightRed: '#f54235', brightGreen: '#99e343', brightYellow: '#fdeb61', brightBlue: '#84b0d8', brightMagenta: '#bc94b7', brightCyan: '#37e6e8', brightWhite: '#f1f1f0' + }; + window.zmTermUrl = base + '?disableLeaveAlert=true&theme=' + encodeURIComponent(JSON.stringify(theme)); + return window.zmTermUrl; + } + function refresh() { return zm.termStatus().then(function(d) { data = d || {}; renderAll(); autoStart(); }); } @@ -19844,14 +20067,20 @@ return view.extend({ var show = !!data.installed && !!data.running && canEmbed && !busy; if (keep) { viewEl.style.display = 'none'; - if (!show) keep.hide(); else keep.open(addr()); + if (!show) keep.hide(); else keep.open(frameUrl(), termTitle()); return; } if (!show) { viewEl.style.display = 'none'; return; } if (!frame) { - frame = E('iframe', { 'src': addr(), 'style': 'width:100%; height:480px; border:0; border-radius:8px; background:#000' }); + frame = E('iframe', { 'class': 'zm-term-frame', 'src': frameUrl() }); viewEl.innerHTML = ''; - viewEl.appendChild(frame); + viewEl.appendChild(E('div', { 'class': 'zm-term' }, [ + E('div', { 'class': 'zm-term-bar' }, [ + E('span', { 'class': 'zm-term-dots' }, [ E('i', {}), E('i', {}), E('i', {}) ]), + E('span', { 'class': 'zm-term-title' }, termTitle()) + ]), + E('div', { 'class': 'zm-term-body' }, [ frame ]) + ])); } viewEl.style.display = ''; } @@ -20268,13 +20497,17 @@ return view.extend({ if (data.blocker) return; listCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Нажмите на пункт, чтобы включить или выключить его, и затем «Сохранить и применить». Списки берутся из каталога списков (itdoginfo/allow-domains, b4geoip и другие) и обновляются при каждом применении.')); var cur = currentPick(), sel = pick || cur, changed = false; - var list = data.services || []; + var all = data.services || []; + var list = all.filter(function(s) { return s.id !== 'custom'; }).map(function(s) { + if (!/^c_itdoginfo_/.test(s.id) && !/^itdoginfo/i.test(s.group || '')) return s; + return { id: s.id, name: s.name, group: '', cat: true, on: s.on, skip: s.skip }; + }); list.forEach(function(s) { if (!!sel[s.id] !== !!cur[s.id]) changed = true; }); function tile(s) { return zm.svcCard({ name: s.name, key: s.id, on: !!sel[s.id], inc: !!sel.russia_inside && s.id !== 'russia_inside' && !s.group && zm.riCovers(s.id), click: function() { if (busy) { zm.toast('Дождитесь окончания текущей операции', 'warning'); return; } pick = {}; - for (var k in sel) if (sel[k]) pick[k] = true; + for (var k in sel) if (sel[k] && k !== 'custom') pick[k] = true; if (pick[s.id]) delete pick[s.id]; else pick[s.id] = true; renderLists(); } }); @@ -20334,7 +20567,7 @@ return view.extend({ saveLabel: data.installed && !data.stopped ? 'Сохранить и применить' : 'Сохранить', onCancel: function() { pick = null; renderLists(); }, onSave: function() { - var ids = list.filter(function(s) { return sel[s.id]; }).map(function(s) { return s.id; }); + var ids = all.filter(function(s) { return s.id === 'custom' ? cur.custom : sel[s.id]; }).map(function(s) { return s.id; }); act('lists', ids.join(','), 'Применяем выбор — скачиваем списки'); } }); @@ -20384,18 +20617,19 @@ return view.extend({ customCard.style.display = data.blocker || !svc ? 'none' : ''; if (data.blocker || !svc) return; customCard.appendChild(E('h3', {}, 'Свой список')); - customCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Домены и IP-адреса из этого списка пойдут через Steer. После сохранения список сразу включается в выбор сервисов, «Очистить» — убирает его.')); + customCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Домены и IP-адреса из этого списка пойдут через Steer. По умолчанию список выключен — сохраните его и включите переключателем ниже. «Очистить» — убирает его.')); if (customLoading && !customData) { customCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Загружаем список…')); return; } var n = customData ? (parseInt(customData.count, 10) || 0) : 0; customCard.appendChild(zm.swRow(!!svc.on, 'Свой список', svc.on ? 'Включён: домены и IP-адреса из списка идут через Steer. Выключите — они пойдут напрямую, сам список останется.' : 'Выключен: список не используется. Включите — домены и IP-адреса из него пойдут через Steer.', function() { if (busy) { zm.toast('Дождитесь окончания текущей операции', 'warning'); return; } if (!svc.on && !n) { zm.toast('Сначала добавьте в список домены или IP', 'warning'); return; } + if (customDirty) { zm.toast('Сначала сохраните изменения в списке', 'warning'); return; } var ids = (data.services || []).filter(function(s) { return s.on && s.id !== 'custom'; }).map(function(s) { return s.id; }); if (!svc.on) ids.push('custom'); act('lists', ids.join(','), svc.on ? 'Выключаем свой список' : 'Включаем свой список'); }, busy)); customCard.appendChild(row('Состояние', !n ? badge('zm-off', 'список пуст') - : !svc.on ? badge('zm-warn', 'не выбран') + : !svc.on ? badge('zm-warn', 'выключен') : !data.installed ? badge('zm-warn', 'пойдёт через Steer после установки') : data.stopped ? badge('zm-off', 'Steer выключен') : data.exit === 'vpn' ? badge('zm-ok', 'идёт через VPN') @@ -21174,6 +21408,8 @@ var LABELS = { rutor: [ 'rutor.info', 'Торренты' ], rutracker: [ 'rutracker.org', 'Торренты' ], scell: [ 'Supercell', 'Clash, Brawl Stars' ], + roblox: [ 'Картинки Roblox', 'tr.rbxcdn.com — аватары и превью' ], + riot: [ 'Riot Games', 'Вход в Valorant и League of Legends' ], githubraw: [ 'GitHub Raw', 'githubusercontent.com' ], github: [ 'GitHub', 'Код и релизы' ], tapeop: [ 'tapeop.dev', 'Сайт' ], @@ -22178,6 +22414,10 @@ return view.extend({ 'class': 'cbi-button', 'click': function() { doZapretAction(d.zapret_running ? 'stop' : 'start'); } }, d.zapret_running ? 'Остановить' : 'Запустить')); + if (d.zapret_running) zActions.push(E('button', { + 'class': 'cbi-button', + 'click': function() { doZapretAction('restart'); } + }, 'Перезапустить')); } else { zActions.push(E('button', { 'class': 'cbi-button cbi-button-positive', @@ -22214,7 +22454,7 @@ return view.extend({ if (zBusy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } var job = (action === 'install' || action === 'update') ? 'install_zapret' : (action === 'remove') ? 'remove_zapret' : null; - var LABELS = { install: 'Устанавливаем и настраиваем Zapret', update: 'Обновляем Zapret', remove: 'Удаляем Zapret', start: 'Запускаем Zapret', stop: 'Останавливаем Zapret' }; + var LABELS = { install: 'Устанавливаем и настраиваем Zapret', update: 'Обновляем Zapret', remove: 'Удаляем Zapret', start: 'Запускаем Zapret', stop: 'Останавливаем Zapret', restart: 'Перезапускаем Zapret' }; zm.toast(LABELS[action] || 'Выполняем', 'warning'); if (job) zBusy = true; @@ -22232,6 +22472,7 @@ return view.extend({ }); } else if (res) { zBusy = false; + if (action === 'restart') zm.toast(res.zapret_running ? 'Zapret перезапущен' : 'Zapret не запустился — проверьте стратегию', res.zapret_running ? 'info' : 'error'); status = res; renderZCard(res); renderBanner(); @@ -23616,6 +23857,10 @@ return view.extend({ 'class': 'cbi-button', 'click': function() { view.doAction2(d.zapret2_running ? 'stop' : 'start'); } }, d.zapret2_running ? 'Остановить' : 'Запустить')); + if (d.zapret2_running) z2Actions.push(E('button', { + 'class': 'cbi-button', + 'click': function() { view.doAction2('restart'); } + }, 'Перезапустить')); } else { z2Actions.push(E('button', { 'class': 'cbi-button cbi-button-positive', @@ -23661,7 +23906,7 @@ return view.extend({ if (view.busy2) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } var job = (action === 'install' || action === 'update') ? 'install_zapret2' : (action === 'remove') ? 'remove_zapret2' : null; - var LABELS = { install: 'Устанавливаем Zapret2', update: 'Обновляем Zapret2', remove: 'Удаляем Zapret2', start: 'Запускаем Zapret2', stop: 'Останавливаем Zapret2' }; + var LABELS = { install: 'Устанавливаем Zapret2', update: 'Обновляем Zapret2', remove: 'Удаляем Zapret2', start: 'Запускаем Zapret2', stop: 'Останавливаем Zapret2', restart: 'Перезапускаем Zapret2' }; zm.toast(LABELS[action] || 'Выполняем', 'warning'); if (job) view.busy2 = true; @@ -23679,6 +23924,7 @@ return view.extend({ }); } else if (res) { view.busy2 = false; + if (action === 'restart') zm.toast(res.zapret2_running ? 'Zapret2 перезапущен' : 'Zapret2 не запустился', res.zapret2_running ? 'info' : 'error'); view.renderCards(res); } }).catch(function() { view.busy2 = false; }); @@ -23836,6 +24082,34 @@ html.zm-theme-dark .zm-tile:not(.zm-active):not(.zm-tile-off) { box-shadow: inset 0 0 0 1px rgba(0,0,0,.2); } .zm-log.zm-show { display: block; } +.zm-term { + box-sizing: border-box; min-width: 0; overflow: hidden; + background: #0d1117; color: #8b949e; + border: 1px solid rgba(255,255,255,.10); border-radius: 10px; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.2); +} +.zm-term-bar { + display: flex; align-items: center; gap: 12px; + height: 38px; padding: 0 16px; box-sizing: border-box; + border-bottom: 1px solid rgba(255,255,255,.07); + font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", monospace; + font-size: 12.5px; line-height: 1; color: #8b949e; user-select: none; +} +.zm-term-dots { display: inline-flex; gap: 8px; flex: 0 0 auto; } +.zm-term-dots i { display: block; width: 10px; height: 10px; border-radius: 50%; background: #ff5f57; } +.zm-term-dots i:nth-child(2) { background: #febc2e; } +.zm-term-dots i:nth-child(3) { background: #28c840; } +.zm-term-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.zm-term-body { padding: 8px 11px 10px; background: inherit; } +.zm-term-frame { + display: block; width: 100%; border: 0; background: #0d1117; + height: 480px; height: clamp(320px, calc(100vh - 340px), 780px); +} +@media (max-width: 600px) { + .zm-term-bar { padding: 0 12px; } + .zm-term-body { padding: 6px 5px 8px; } + .zm-term-frame { height: clamp(280px, calc(100vh - 300px), 620px); } +} .zm-log { scrollbar-width: auto; scrollbar-color: auto; } .zm-log::-webkit-scrollbar { width: 14px; } .zm-log::-webkit-scrollbar-track { background: transparent; margin: 8px 0; } @@ -24798,6 +25072,51 @@ return view.extend({ ]); wrap.appendChild(mirrorCard); + var cleanLog = E('pre', { 'class': 'zm-log' }); + var cleanBusy = false; + function fmtKb(k) { + k = parseInt(k, 10) || 0; + if (k < 1024) return k + ' КБ'; + return (k / 1024).toFixed(k < 10240 ? 1 : 0) + ' МБ'; + } + var cleanCard = E('div', { 'class': 'zm-card' }, [ + E('h3', {}, 'Очистка Zapret Manager'), + E('p', { 'class': 'zm-hint' }, 'Удаляет с роутера временные файлы и мусор панели. Настройки и установленные программы не трогает.'), + E('div', { 'class': 'zm-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + if (cleanBusy) { zm.toast('Очистка уже идёт', 'warning'); return; } + cleanBusy = true; + cleanLog.classList.add('zm-show'); + zm.renderLog(cleanLog, '==> Удаляем временные файлы и мусор панели'); + zm.zmCleanup().then(function(res) { + cleanBusy = false; + if (!res || res.error) { + var why = (res && res.error) || 'роутер не ответил'; + zm.renderLog(cleanLog, '==> Очистка не выполнена: ' + why); + zm.toast(why, 'error'); + return; + } + var lines = [ '==> Удаляем временные файлы и мусор панели', + ' ✓ Удалено файлов и папок: ' + (parseInt(res.removed, 10) || 0), + ' ✓ Освобождено на флеше: ' + fmtKb(res.freed_flash_kb), + ' ✓ Освобождено в ОЗУ: ' + fmtKb((parseInt(res.freed_tmp_kb, 10) || 0) + (parseInt(res.freed_mem_kb, 10) || 0)), + '==> Готово' ]; + zm.renderLog(cleanLog, lines.join('\n')); + zm.toast('Очистка выполнена', 'info'); + }).catch(function() { + cleanBusy = false; + zm.renderLog(cleanLog, '==> Очистка не выполнена: роутер не ответил'); + zm.toast('Роутер не ответил', 'error'); + }); + } + }, 'Очистить') + ]), + cleanLog + ]); + wrap.appendChild(cleanCard); + var uninstallLog = E('pre', { 'class': 'zm-log' }); var uninstallCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Удалить Zapret Manager'), @@ -25550,7 +25869,7 @@ function fromCfg(c) { byDomains: ((c.bypass && c.bypass.domains) || []).join('\n'), bySubnets: ((c.bypass && c.bypass.subnets) || []).join('\n'), mode: c.mode || 'links', links: (c.links || []).join('\n'), sub: (c.subs && c.subs.length ? c.subs : c.sub ? [ c.sub ] : []).join('\n'), sub_interval: c.sub_interval || '12h', iface: c.iface || '', fastest: true, exclude: c.exclude || '', - items: pick.items, sel: pick.sel, own_on: c.own_on !== false, domains: (c.domains || []).join('\n'), subnets: (c.subnets || []).join('\n'), + items: pick.items, sel: pick.sel, own_on: c.own_on !== false && ((c.domains || []).length + (c.subnets || []).length + pick.lists.length) > 0, domains: (c.domains || []).join('\n'), subnets: (c.subnets || []).join('\n'), lists: pick.lists.join('\n'), full: (c.full || []).slice(), excl: (c.excl || []).slice(), dns: { type: dns.type || (dns.server ? 'udp' : 'dot'), servers: (dns.servers && dns.servers.length ? dns.servers : [ dns.server || '9.9.9.9' ]).slice(), bootstraps: (dns.bootstraps && dns.bootstraps.length ? dns.bootstraps : [ dns.bootstrap || '9.9.9.9' ]).slice(), detour: !!dns.detour }, @@ -25644,7 +25963,7 @@ function newCfg(c) { var n = ((c && c.sections) || []).length + 1; x.sec = 'new'; x.is_main = false; x.exists = false; x.enabled = true; x.label = warp && /^zmwarp/.test(warp.name) && !((c && c.sections) || []).some(function(s) { return s.label === 'WARP'; }) ? 'WARP' : 'Секция ' + n; x.mode = 'iface'; x.iface = warp ? warp.name : ''; x.links = []; x.sub = ''; x.sub_interval = '12h'; x.exclude = ''; x.hide_names = []; - x.refs = { c: [], s: [], r: [] }; x.own_on = true; x.domains = []; x.subnets = []; x.lists = []; x.subs = []; x.x = {}; + x.refs = { c: [], s: [], r: [] }; x.own_on = false; x.domains = []; x.subnets = []; x.lists = []; x.subs = []; x.x = {}; x.full = []; return x; } @@ -26446,7 +26765,7 @@ return view.extend({ function lanIp() { return netInfo().lan_ip || 'IP-роутера'; } function devFoot(d) { - var k = { bridge: 'мост', eth: 'порт', ppp: 'PPP', tun: 'туннель', wifi: 'Wi-Fi' }[d.kind] || ''; + var k = { bridge: 'мост', eth: 'порт', ppp: 'PPP', tun: ({ wireguard: 'WireGuard', amneziawg: 'AmneziaWG' }[d.proto] || 'туннель'), wifi: 'Wi-Fi' }[d.kind] || ''; return (d.net ? 'сеть ' + d.net + (k ? ' · ' + k : '') : k) + (d.missing ? ' · не найден' : ''); } @@ -26895,7 +27214,8 @@ return view.extend({ !ownCnt ? badge('zm-off', 'пуст') : ownOn ? badge('zm-ok', 'включён · ' + nn(ownCnt, 'строка', 'строки', 'строк')) : badge('zm-warn', 'выключен · ' + nn(ownCnt, 'строка', 'строки', 'строк')) ])); ownCard.appendChild(zm.swRow(ownOn, 'Вести свой список через VPN', ownOn ? 'Свои домены, адреса и внешние списки идут через подключение секции — так же, как выбранные сервисы.' - : 'Выключено: свои домены, адреса и внешние списки сохранены, но идут напрямую. Включите — и они снова пойдут через VPN.', + : ownCnt ? 'Выключено: свои домены, адреса и внешние списки сохранены, но идут напрямую. Включите — и они снова пойдут через VPN.' + : 'По умолчанию выключено. Впишите ниже домены, адреса или ссылки на списки и включите — они пойдут через VPN.', function() { set('own_on', !ownOn); })); var ownDim = ownOn ? '' : 'opacity:.55'; [ taDomains, taSubnets, taLists ].forEach(function(t) { t.style.opacity = ownOn ? '' : '.55'; }); @@ -27145,8 +27465,13 @@ return view.extend({ netCard.appendChild(E('h3', {}, 'Сеть')); netCard.appendChild(E('h4', { 'style': 'margin:4px 0' }, 'Откуда принимать трафик')); - netCard.appendChild(hint('Устройства из этих сетей идут через Forkozz. Обычно это br-lan — вся домашняя сеть. Гостевую или отдельную сеть можно добавить сюда же.', 0)); - var src = withMissing(devs.filter(function(d) { return (d.kind === 'bridge' || d.kind === 'eth') && !isWan(d); }), draft.src_ifs); + netCard.appendChild(hint('Устройства из этих сетей идут через Forkozz. Обычно это br-lan — вся домашняя сеть. Гостевую или отдельную сеть можно добавить сюда же. Свой VPN-сервер на роутере (WireGuard, AmneziaWG, OpenVPN) тоже можно отметить — тогда его клиенты пойдут через Forkozz.', 0)); + var outIfs = {}; + secs().forEach(function(s) { if (s.iface) outIfs[s.iface] = true; }); + if (draft.mode === 'iface' && draft.iface) outIfs[draft.iface] = true; + (cfg && cfg.tunnels || []).forEach(function(t) { if (t.steer) outIfs[t.name] = true; }); + var isSrcTun = function(d) { return d.kind === 'tun' && !outIfs[d.name] && !/^(zmwarp|zm_|zmv_)/.test(d.name); }; + var src = withMissing(devs.filter(function(d) { return (d.kind === 'bridge' || d.kind === 'eth' || isSrcTun(d)) && !isWan(d); }), draft.src_ifs); netCard.appendChild(devTiles(src, function(n) { return draft.src_ifs.indexOf(n) >= 0; }, function(n, on) { if (on && draft.src_ifs.length === 1) { zm.toast('Нужна хотя бы одна сеть', 'warning'); return; } set('src_ifs', on ? draft.src_ifs.filter(function(x) { return x !== n; }) : draft.src_ifs.concat([ n ])); @@ -29481,7 +29806,7 @@ var ROUTES = [ { id: 'hosts', title: 'Hosts', sub: 'Домены в hosts и списки GeoHide', icon: 'list', group: 'Сеть', dot: 'hosts' }, { 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: 'terminal', title: 'Терминал', sub: 'Командная строка роутера в браузере (ttyd)', icon: 'terminal', group: 'Сеть' }, + { id: 'terminal', title: 'Терминал', sub: 'Командная строка роутера в браузере (ttyd)', icon: 'terminal', group: 'Сеть', dot: 'term' }, { id: 'system', title: 'Система', sub: 'Параметры роутера, зеркала и обслуживание', icon: 'cpu', group: 'Сервис' } ]; var ROUTE_BY_ID = {}; @@ -29498,7 +29823,7 @@ var alertsEl = E('div', { 'class': 'zmw-alerts' }), alertsKey = ''; /* Терминал переживает переходы между разделами: его iframe живёт в viewEl постоянно и только прячется. * Прячем без display:none и без смены размера — иначе ttyd сожмёт терминал до нуля колонок. */ var termKeep = { host: null, frame: null, src: '' }; -var TERM_SHOW = 'max-width:1100px; margin:0 0 16px', TERM_HIDE = 'max-width:1100px; margin:0; height:0; overflow:hidden; visibility:hidden'; +var TERM_SHOW = 'margin:0 0 18px', TERM_HIDE = 'margin:0; height:0; overflow:hidden; visibility:hidden; border-width:0; box-shadow:none'; function termHide() { if (termKeep.host) termKeep.host.setAttribute('style', TERM_HIDE); } @@ -29507,13 +29832,19 @@ function termDrop() { termKeep.host = null; termKeep.frame = null; termKeep.src = ''; } -function termOpen(src) { +function termOpen(src, title) { if (!viewEl || !sid || currentRoute() !== 'terminal') return false; if (termKeep.frame && termKeep.src !== src) termDrop(); if (!termKeep.host || termKeep.host.parentNode !== viewEl) { termDrop(); - termKeep.frame = E('iframe', { 'src': src, 'style': 'display:block; width:100%; height:480px; border:0; border-radius:8px; background:#000' }); - termKeep.host = E('div', { 'class': 'zmw-term-keep', 'style': TERM_HIDE }, [ termKeep.frame ]); + termKeep.frame = E('iframe', { 'class': 'zm-term-frame', 'src': src }); + termKeep.host = E('div', { 'class': 'zmw-term-keep zm-term', 'style': TERM_HIDE }, [ + E('div', { 'class': 'zm-term-bar' }, [ + E('span', { 'class': 'zm-term-dots' }, [ E('i', {}), E('i', {}), E('i', {}) ]), + E('span', { 'class': 'zm-term-title' }, title || 'ttyd') + ]), + E('div', { 'class': 'zm-term-body' }, [ termKeep.frame ]) + ]); termKeep.src = src; viewEl.insertBefore(termKeep.host, viewEl.firstChild); } @@ -29733,7 +30064,7 @@ function refreshMemory() { } var statusBusy = false; -var DOT_TEXT = { 1: 'работает', 2: 'не работает', 3: 'работает частично' }; +var DOT_TEXT = { 1: 'работает', 2: 'ошибка — не работает', 3: 'работает частично', 4: 'установлен, не запущен или не настроен' }; function refreshShellStatus() { if (!shell || !sid || statusBusy) return; statusBusy = true; @@ -29753,13 +30084,14 @@ function refreshShellStatus() { if (k === 'awg' && h.awg_total) title = 'туннелей работает: ' + (h.awg_up || 0) + ' из ' + h.awg_total; else if (k === 'awg' && h.awg === 1) title = 'установлен, туннелей нет'; else if (k === 'awg' && h.awg === 3) title = 'установлен не полностью — откройте вкладку и нажмите «Доустановить»'; - setDot(navDots[k], h[k], title); + if (h.dots && h.dots[k] === 4) title = null; + setDot(navDots[k], h.dots && typeof h.dots[k] === 'number' ? h.dots[k] : h[k], title); }); }).catch(function () {}).then(function () { statusBusy = false; }); } function setDot(el, st, title) { if (!el) return; - el.className = 'zmw-nav-dot' + (st === 1 ? ' zmw-on' : st === 2 ? ' zmw-stop' : st === 3 ? ' zmw-part' : ''); + el.className = 'zmw-nav-dot' + (st === 1 ? ' zmw-on' : st === 2 ? ' zmw-stop' : st === 3 ? ' zmw-part' : st === 4 ? ' zmw-idle' : ''); el.title = title || DOT_TEXT[st] || ''; } @@ -30287,7 +30619,8 @@ body.zmw-locked .zmw-shell { filter: blur(6px); pointer-events: none; } box-shadow: 0 6px 16px -6px rgba(99,102,241,.8); } .zmw-nav-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: none; } -.zmw-nav-dot.zmw-on, .zmw-nav-dot.zmw-stop, .zmw-nav-dot.zmw-part { display: block; } +.zmw-nav-dot.zmw-on, .zmw-nav-dot.zmw-stop, .zmw-nav-dot.zmw-part, .zmw-nav-dot.zmw-idle { display: block; } +.zmw-nav-dot.zmw-idle { background: #9aa3b2; box-shadow: 0 0 0 3px rgba(154,163,178,.18); } .zmw-nav-dot.zmw-part { background: var(--warn-dot); box-shadow: 0 0 0 3px var(--warn-bg), 0 0 10px var(--warn-dot); } .zmw-nav-dot.zmw-on { background: var(--ok-dot); box-shadow: 0 0 0 3px var(--ok-bg), 0 0 10px var(--ok-dot); } .zmw-nav-dot.zmw-stop { background: var(--bad-dot); box-shadow: 0 0 0 3px var(--bad-bg), 0 0 10px var(--bad-dot); animation: zmw-blink 1.6s ease-in-out infinite; } @@ -30848,6 +31181,17 @@ html[data-theme="dark"] #zmw-view .zm-tile.zm-active::before { color: #a594ff; } #zmw-view .bt-chip { border-radius: 7px; } #zmw-view iframe { border: 1px solid var(--border-2) !important; border-radius: 16px !important; background: #fff; box-shadow: var(--shadow); } +#zmw-view .zm-term { + background: var(--console); border: 1px solid var(--console-border); border-radius: 16px; + box-shadow: inset 0 1px 0 rgba(255,255,255,.04), var(--shadow); +} +#zmw-view .zm-term-bar { height: 40px; padding: 0 20px; font-family: var(--mono); } +#zmw-view .zm-term-body { padding: 8px 15px 12px; } +#zmw-view .zm-term iframe.zm-term-frame { border: 0 !important; border-radius: 0 !important; background: var(--console); box-shadow: none; } +@media (max-width: 600px) { + #zmw-view .zm-term-bar { padding: 0 14px; } + #zmw-view .zm-term-body { padding: 6px 9px 10px; } +} #zmw-view pre:not(.zm-log) { font-family: var(--mono); } #zmw-view hr { border: 0; border-top: 1px solid var(--border); } @@ -31577,6 +31921,14 @@ if [ -x /usr/bin/netshift ] && [ -f /usr/lib/netshift/constants.sh ]; then else _zmi_warn "Сверить не удалось — откройте вкладку Forkozz и нажмите «Перезапустить»"; fi fi +_zmi_say "Убираем временные файлы и остатки прошлых версий" +_zm_cl="$(/opt/zapret-manager-luci/backend.sh zm_cleanup install 2>/dev/null || true)" +_zm_cl_f="$(printf '%s' "$_zm_cl" | sed -n 's/.*"freed_flash_kb":\([0-9]*\).*/\1/p')" +_zm_cl_t="$(printf '%s' "$_zm_cl" | sed -n 's/.*"freed_tmp_kb":\([0-9]*\).*/\1/p')" +_zm_cl_s="$(printf '%s' "$_zm_cl" | sed -n 's/.*"flash_kb":\([0-9]*\).*/\1/p')" +if [ -n "$_zm_cl_s" ]; then _zmi_ok "Освобождено: флеш ${_zm_cl_f:-0} КБ, /tmp ${_zm_cl_t:-0} КБ. Панель занимает на флеше ${_zm_cl_s} КБ" +else _zmi_ok "Готово"; fi + ZMW_PORT="$(uci -q get uhttpd.zmweb.listen_http | tr ' ' '\n' | head -n1 | sed 's/.*://')" ZMW_IP="$(/opt/zapret-manager-luci/backend.sh lan_ip 2>/dev/null || true)" [ -n "$ZMW_IP" ] || ZMW_IP="192.168.1.1"