Загрузка данных


"""
ZabHelper v4.3 — Центр восстановления Windows.
UI: синий glassmorphism (сайдбар + карточки).
Раздел "Ограничения": сканирование (только чтение) + РУЧНОЕ редактирование
числовых значений параметров реестра. Автоснятия и автоудаления нет.
"""

from __future__ import annotations

import base64
import csv
import ctypes
import datetime
import glob
import json
import os
import re
import shutil
import subprocess
import sys
import threading
import tkinter as tk
from tkinter import filedialog, ttk, scrolledtext
from typing import Any, Callable, Optional

try:
    import winreg  # type: ignore
except ImportError:
    winreg = None  # type: ignore


# ====================================================================== #
#  ПАЛИТРА: СИНИЙ GLASSMORPHISM
# ====================================================================== #
PALETTE = {
    "bg_deep":     "#070b1c",
    "bg_mid":      "#0e1630",
    "bg_glass":    "#101c3a",
    "bg_card":     "#0f1c40",
    "bg_card_hi":  "#152a5c",
    "bg_sidebar":  "#0a1228",
    "bg_input":    "#0c1836",
    "border":      "#1f3a75",
    "border_hi":   "#2c5bb8",

    "fg":          "#eaf1ff",
    "fg_dim":      "#8aa2d6",
    "fg_mute":     "#5f74a8",

    "accent":      "#4c8dff",
    "accent_hi":   "#7aa8ff",
    "accent_deep": "#1b47a8",
    "glow":        "#6ea8ff",

    "success":     "#4ade80",
    "warning":     "#fbbf24",
    "error":       "#f87171",
    "sys":         "#a78bfa",
    "hidden":      "#6b7ba8",
}

FONT_UI    = ("Segoe UI", 10)
FONT_UI_B  = ("Segoe UI", 10, "bold")
FONT_H1    = ("Segoe UI Semibold", 16, "bold")
FONT_H2    = ("Segoe UI Semibold", 12, "bold")
FONT_MONO  = ("Consolas", 9)
FONT_SMALL = ("Segoe UI", 9)


# ====================================================================== #
#  ЗАПУСК КОМАНД С ПРАВИЛЬНОЙ КОДИРОВКОЙ
# ====================================================================== #
class ConsoleRunner:
    """Единая точка запуска cmd/powershell с корректным декодированием."""

    UTF8_BOM     = b"\xef\xbb\xbf"
    UTF16_LE_BOM = b"\xff\xfe"
    UTF16_BE_BOM = b"\xfe\xff"
    CREATE_NO_WINDOW = 0x08000000 if sys.platform == "win32" else 0

    @staticmethod
    def _decode(raw: bytes) -> str:
        if not raw:
            return ""
        if raw.startswith(ConsoleRunner.UTF8_BOM):
            return raw[3:].decode("utf-8", errors="replace")
        if raw.startswith(ConsoleRunner.UTF16_LE_BOM):
            return raw[2:].decode("utf-16-le", errors="replace")
        if raw.startswith(ConsoleRunner.UTF16_BE_BOM):
            return raw[2:].decode("utf-16-be", errors="replace")
        if len(raw) >= 4 and raw[1::2].count(0) > len(raw) // 4:
            try:
                return raw.decode("utf-16-le")
            except UnicodeDecodeError:
                pass
        for enc in ("utf-8", "cp866", "cp1251"):
            try:
                return raw.decode(enc)
            except UnicodeDecodeError:
                continue
        return raw.decode("utf-8", errors="replace")

    @staticmethod
    def run(command: str, timeout: int = 120, force_utf8: bool = True) -> str:
        full = f'chcp 65001 >nul && {command}' if force_utf8 else command
        try:
            result = subprocess.run(
                full, shell=True, capture_output=True, timeout=timeout,
                creationflags=ConsoleRunner.CREATE_NO_WINDOW,
            )
            out = ConsoleRunner._decode(result.stdout or b"")
            err = ConsoleRunner._decode(result.stderr or b"")
            if err.strip():
                out += "\n" + err
            return out
        except subprocess.TimeoutExpired:
            return "[таймаут]"
        except Exception as e:
            return f"[ошибка: {e}]"

    @staticmethod
    def run_ps(script: str, timeout: int = 120) -> str:
        try:
            encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
        except Exception:
            return "[ошибка кодирования PowerShell]"
        full = (
            'chcp 65001 >nul && powershell -NoProfile -ExecutionPolicy Bypass '
            f'-EncodedCommand {encoded}'
        )
        try:
            result = subprocess.run(
                full, shell=True, capture_output=True, timeout=timeout,
                creationflags=ConsoleRunner.CREATE_NO_WINDOW,
            )
            return ConsoleRunner._decode(result.stdout or b"")
        except subprocess.TimeoutExpired:
            return "[таймаут PowerShell]"
        except Exception as e:
            return f"[ошибка PowerShell: {e}]"


# ====================================================================== #
#  ГЛАВНОЕ ПРИЛОЖЕНИЕ
# ====================================================================== #
class ZabHelperApp:
    """ZabHelper v4.3: раздел «Ограничения» — только ручное редактирование чисел."""

    SECTIONS: list[tuple[str, str, str]] = [
        ("monitor",  "◉",  "Мониторинг"),
        ("running",  "▷",  "Запущенные"),
        ("services", "⚙",  "Службы"),
        ("startup",  "▲",  "Автозагрузка"),
        ("tweaks",   "◇",  "Твики"),
        ("repair",   "✚",  "Восстановление"),
        ("network",  "⌬",  "Сеть"),
        ("security", "⛨",  "Безопасность"),
        ("restrict", "⚿",  "Ограничения"),
        ("open",     "↗",  "Открыть"),
        ("backups",  "▣",  "Бэкапы"),
        ("diag",     "◎",  "Диагностика"),
    ]

    def __init__(self, root: tk.Tk) -> None:
        self.root = root
        self.root.title("ZabHelper v4.3 — Центр восстановления Windows")
        self.root.geometry("1320x880")
        self.root.minsize(1120, 720)
        self.root.configure(bg=PALETTE["bg_mid"])

        self.is_admin: bool = self._check_admin_rights()
        self._proc_cache: list[dict[str, Any]] = []
        self._running_cache: list[dict[str, Any]] = []
        self._startup_cache: list[dict[str, Any]] = []
        self._restrict_cache: list[dict[str, Any]] = []
        self._proc_sort_col: Optional[str] = None
        self._proc_sort_rev: bool = False
        self._current_section: str = "monitor"
        self._section_buttons: dict[str, tk.Frame] = {}
        self._section_frames: dict[str, tk.Frame] = {}
        self._confirm_var = tk.IntVar(value=0)
        self._confirm_result: Optional[bool] = None
        self._toast_after: Optional[str] = None
        self._auto_refresh: bool = False
        self._auto_refresh_after: Optional[str] = None

        self._setup_styles()
        self._build_layout()
        self._bind_hotkeys()

        self.log("ZabHelper v4.3 запущен.", "ok")
        self.log(f"Права администратора: {'ДА' if self.is_admin else 'НЕТ'}",
                 "ok" if self.is_admin else "warn")
        if not self.is_admin:
            self.toast("Запущено без прав администратора.", "warn", 5000)

        self.show_section("monitor")

    # ------------------------------------------------------------------ #
    #  ПРАВА
    # ------------------------------------------------------------------ #
    @staticmethod
    def _check_admin_rights() -> bool:
        try:
            return bool(ctypes.windll.shell32.IsUserAnAdmin())
        except Exception:
            return False

    def _restart_as_admin(self) -> None:
        try:
            params = " ".join(f'"{a}"' for a in sys.argv)
            ctypes.windll.shell32.ShellExecuteW(
                None, "runas", sys.executable, params, None, 1)
            self.root.destroy()
        except Exception as e:
            self.log(f"Не удалось перезапустить: {e}", "err")

    # ------------------------------------------------------------------ #
    #  СТИЛИ TTK
    # ------------------------------------------------------------------ #
    def _setup_styles(self) -> None:
        style = ttk.Style()
        try:
            style.theme_use("clam")
        except tk.TclError:
            pass

        style.configure(".", background=PALETTE["bg_mid"], foreground=PALETTE["fg"],
                        fieldbackground=PALETTE["bg_input"], borderwidth=0,
                        font=FONT_UI)
        style.configure("TFrame", background=PALETTE["bg_mid"])
        style.configure("Card.TFrame", background=PALETTE["bg_card"])
        style.configure("TLabel", background=PALETTE["bg_mid"], foreground=PALETTE["fg"])

        style.configure("TButton",
                        background=PALETTE["bg_card"], foreground=PALETTE["fg"],
                        borderwidth=1, focusthickness=0, padding=(10, 8),
                        font=FONT_UI)
        style.map("TButton",
                  background=[("active", PALETTE["bg_card_hi"]),
                              ("pressed", PALETTE["accent_deep"])],
                  foreground=[("active", PALETTE["fg"])])

        style.configure("Accent.TButton",
                        background=PALETTE["accent_deep"], foreground="#ffffff",
                        font=FONT_UI_B, borderwidth=0)
        style.map("Accent.TButton",
                  background=[("active", PALETTE["accent"]),
                              ("pressed", PALETTE["accent_hi"])])

        style.configure("Danger.TButton",
                        background="#3b0d1a", foreground=PALETTE["error"],
                        font=FONT_UI_B, borderwidth=0)
        style.map("Danger.TButton", background=[("active", "#5a1325")])

        style.configure("Success.TButton",
                        background="#0c2a1a", foreground=PALETTE["success"],
                        font=FONT_UI_B, borderwidth=0)
        style.map("Success.TButton", background=[("active", "#124a2b")])

        style.configure("Ghost.TButton",
                        background=PALETTE["bg_mid"], foreground=PALETTE["fg_dim"],
                        borderwidth=0, padding=(8, 6))
        style.map("Ghost.TButton",
                  background=[("active", PALETTE["bg_card"])],
                  foreground=[("active", PALETTE["fg"])])

        style.configure("Treeview",
                        background=PALETTE["bg_card"], foreground=PALETTE["fg"],
                        fieldbackground=PALETTE["bg_card"], borderwidth=0,
                        font=FONT_UI, rowheight=26)
        style.configure("Treeview.Heading",
                        background=PALETTE["bg_glass"], foreground=PALETTE["accent_hi"],
                        font=FONT_UI_B, relief="flat", padding=(6, 6))
        style.map("Treeview",
                  background=[("selected", PALETTE["accent_deep"])],
                  foreground=[("selected", "#ffffff")])
        style.map("Treeview.Heading",
                  background=[("active", PALETTE["bg_card_hi"])])

        style.configure("TProgressbar",
                        background=PALETTE["accent"], troughcolor=PALETTE["bg_card"],
                        borderwidth=0, thickness=6)

        style.configure("TEntry",
                        fieldbackground=PALETTE["bg_input"],
                        foreground=PALETTE["fg"], insertcolor=PALETTE["accent"],
                        borderwidth=0, padding=6)

        style.configure("TCheckbutton",
                        background=PALETTE["bg_card"], foreground=PALETTE["fg"],
                        focuscolor=PALETTE["accent"])
        style.map("TCheckbutton",
                  background=[("active", PALETTE["bg_card"])],
                  foreground=[("active", PALETTE["accent_hi"])])

    # ------------------------------------------------------------------ #
    #  КАРКАС
    # ------------------------------------------------------------------ #
    def _build_layout(self) -> None:
        self._build_sidebar()
        self._build_main_area()
        self._build_statusbar()
        self._build_confirm_bar()
        self._build_toast()

    def _build_sidebar(self) -> None:
        bar = tk.Frame(self.root, bg=PALETTE["bg_sidebar"], width=240)
        bar.pack(side=tk.LEFT, fill=tk.Y)
        bar.pack_propagate(False)

        head = tk.Frame(bar, bg=PALETTE["bg_sidebar"])
        head.pack(fill=tk.X, padx=18, pady=(20, 14))
        logo = tk.Canvas(head, width=42, height=42, bg=PALETTE["bg_sidebar"],
                         highlightthickness=0)
        logo.pack(side=tk.LEFT)
        logo.create_oval(2, 2, 40, 40, fill=PALETTE["accent_deep"], outline="")
        logo.create_text(21, 21, text="⚡", fill="#ffffff",
                         font=("Segoe UI", 16, "bold"))
        title_box = tk.Frame(head, bg=PALETTE["bg_sidebar"])
        title_box.pack(side=tk.LEFT, padx=(10, 0))
        tk.Label(title_box, text="ZabHelper", bg=PALETTE["bg_sidebar"],
                 fg=PALETTE["fg"], font=FONT_H2).pack(anchor="w")
        tk.Label(title_box, text="v4.3 · Windows Care", bg=PALETTE["bg_sidebar"],
                 fg=PALETTE["fg_mute"], font=FONT_SMALL).pack(anchor="w")

        tk.Frame(bar, bg=PALETTE["border"], height=1).pack(fill=tk.X, padx=18, pady=(4, 14))

        nav = tk.Frame(bar, bg=PALETTE["bg_sidebar"])
        nav.pack(fill=tk.BOTH, expand=True, padx=10)
        for key, icon, title in self.SECTIONS:
            item = tk.Frame(nav, bg=PALETTE["bg_sidebar"], cursor="hand2")
            item.pack(fill=tk.X, pady=2)
            icon_lbl = tk.Label(item, text=icon, bg=PALETTE["bg_sidebar"],
                                fg=PALETTE["fg_dim"], font=("Segoe UI", 12), width=3)
            icon_lbl.pack(side=tk.LEFT, padx=(8, 4), pady=8)
            text_lbl = tk.Label(item, text=title, bg=PALETTE["bg_sidebar"],
                                fg=PALETTE["fg_dim"], font=FONT_UI, anchor="w")
            text_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=8)

            for w in (item, icon_lbl, text_lbl):
                w.bind("<Button-1>", lambda e, k=key: self.show_section(k))
                w.bind("<Enter>", lambda e, it=item, il=icon_lbl, tl=text_lbl:
                       self._nav_hover(it, il, tl, True))
                w.bind("<Leave>", lambda e, it=item, il=icon_lbl, tl=text_lbl:
                       self._nav_hover(it, il, tl, False))

            self._section_buttons[key] = item

        bottom = tk.Frame(bar, bg=PALETTE["bg_sidebar"])
        bottom.pack(fill=tk.X, padx=14, pady=14)
        status_color = PALETTE["success"] if self.is_admin else PALETTE["error"]
        status_text = "● Администратор" if self.is_admin else "● Без прав"
        tk.Label(bottom, text=status_text, bg=PALETTE["bg_sidebar"],
                 fg=status_color, font=FONT_UI_B).pack(anchor="w")
        tk.Label(bottom, text="ZabHelper · 2025", bg=PALETTE["bg_sidebar"],
                 fg=PALETTE["fg_mute"], font=FONT_SMALL).pack(anchor="w", pady=(6, 0))

        if not self.is_admin:
            ttk.Button(bottom, text="Запустить от админа",
                       style="Accent.TButton",
                       command=self._restart_as_admin).pack(fill=tk.X, pady=(10, 0))

    def _nav_hover(self, item: tk.Frame, icon_lbl: tk.Label,
                   text_lbl: tk.Label, on: bool) -> None:
        key = next((k for k, v in self._section_buttons.items() if v is item), None)
        if key == self._current_section:
            return
        bg = PALETTE["bg_card"] if on else PALETTE["bg_sidebar"]
        fg = PALETTE["fg"] if on else PALETTE["fg_dim"]
        for w in (item, icon_lbl, text_lbl):
            w.configure(bg=bg)
        icon_lbl.configure(fg=PALETTE["accent"] if on else PALETTE["fg_dim"])
        text_lbl.configure(fg=fg)

    def _build_main_area(self) -> None:
        self.main_area = tk.Frame(self.root, bg=PALETTE["bg_mid"])
        self.main_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        topbar = tk.Frame(self.main_area, bg=PALETTE["bg_mid"])
        topbar.pack(fill=tk.X, padx=22, pady=(18, 6))

        self.section_title = tk.Label(topbar, text="Мониторинг",
                                      bg=PALETTE["bg_mid"], fg=PALETTE["fg"],
                                      font=FONT_H1)
        self.section_title.pack(side=tk.LEFT)

        search_wrap = tk.Frame(topbar, bg=PALETTE["bg_input"],
                               highlightthickness=1,
                               highlightbackground=PALETTE["border"])
        search_wrap.pack(side=tk.RIGHT)
        tk.Label(search_wrap, text="⌕", bg=PALETTE["bg_input"],
                 fg=PALETTE["fg_dim"], font=("Segoe UI", 12)).pack(side=tk.LEFT, padx=(10, 0))
        self.search_var = tk.StringVar()
        search_entry = tk.Entry(search_wrap, textvariable=self.search_var,
                                bg=PALETTE["bg_input"], fg=PALETTE["fg"],
                                insertbackground=PALETTE["accent"],
                                relief="flat", font=FONT_UI, width=32)
        search_entry.pack(side=tk.LEFT, ipady=6, padx=(6, 10))
        search_entry.bind("<Return>", lambda e: self._quick_search())

        self.section_subtitle = tk.Label(self.main_area, text="",
                                         bg=PALETTE["bg_mid"], fg=PALETTE["fg_dim"],
                                         font=FONT_SMALL)
        self.section_subtitle.pack(anchor="w", padx=22, pady=(0, 8))

        self.content = tk.Frame(self.main_area, bg=PALETTE["bg_mid"])
        self.content.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 10))

        builders: list[tuple[str, Callable[[tk.Frame], tk.Frame]]] = [
            ("monitor",  self._build_section_monitor),
            ("running",  self._build_section_running),
            ("services", self._build_section_services),
            ("startup",  self._build_section_startup),
            ("tweaks",   self._build_section_tweaks),
            ("repair",   self._build_section_repair),
            ("network",  self._build_section_network),
            ("security", self._build_section_security),
            ("restrict", self._build_section_restrictions),
            ("open",     self._build_section_open),
            ("backups",  self._build_section_backups),
            ("diag",     self._build_section_diagnostics),
        ]
        for key, builder in builders:
            try:
                self._section_frames[key] = builder(self.content)
            except Exception as e:
                fb = tk.Frame(self.content, bg=PALETTE["bg_mid"])
                tk.Label(fb, text=f"Раздел недоступен: {key}\n{e}",
                         bg=PALETTE["bg_mid"], fg=PALETTE["error"],
                         font=FONT_UI).pack(padx=20, pady=20)
                self._section_frames[key] = fb

        self._build_log_panel(self.main_area)

    def _build_statusbar(self) -> None:
        bar = tk.Frame(self.root, bg=PALETTE["bg_glass"], height=26)
        bar.pack(side=tk.BOTTOM, fill=tk.X)
        color = PALETTE["success"] if self.is_admin else PALETTE["error"]
        tk.Label(bar, text=("● Администратор" if self.is_admin else "● Без прав"),
                 bg=PALETTE["bg_glass"], fg=color,
                 font=FONT_UI_B).pack(side=tk.LEFT, padx=12)
        tk.Label(bar,
                 text="F5 — обновить · Ctrl+S — сохранить лог · Ctrl+L — очистить · Ctrl+Q — выход",
                 bg=PALETTE["bg_glass"], fg=PALETTE["fg_mute"],
                 font=FONT_SMALL).pack(side=tk.LEFT, padx=20)
        self.clock = tk.Label(bar, text="", bg=PALETTE["bg_glass"],
                              fg=PALETTE["fg_dim"], font=FONT_MONO)
        self.clock.pack(side=tk.RIGHT, padx=12)
        self._tick_clock()

    def _tick_clock(self) -> None:
        self.clock.config(text=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
        self.root.after(1000, self._tick_clock)

    def _build_confirm_bar(self) -> None:
        self.confirm_frame = tk.Frame(self.root, bg=PALETTE["bg_glass"],
                                      highlightthickness=1,
                                      highlightbackground=PALETTE["border_hi"])
        self.confirm_label = tk.Label(self.confirm_frame, text="",
                                      bg=PALETTE["bg_glass"], fg=PALETTE["warning"],
                                      font=FONT_UI_B, wraplength=900, justify="left")
        self.confirm_label.pack(side=tk.LEFT, padx=14, pady=10)
        ttk.Button(self.confirm_frame, text="Да", style="Success.TButton",
                   command=lambda: self._resolve_confirm(True)
                   ).pack(side=tk.RIGHT, padx=6, pady=8)
        ttk.Button(self.confirm_frame, text="Нет", style="Danger.TButton",
                   command=lambda: self._resolve_confirm(False)
                   ).pack(side=tk.RIGHT, padx=6, pady=8)

    def ask_confirm(self, text: str) -> bool:
        self.confirm_label.config(text=text)
        self.confirm_frame.pack(side=tk.TOP, fill=tk.X, padx=0, pady=0)
        self.confirm_frame.lift()
        self._confirm_var.set(0)
        self._confirm_result = None
        self.root.wait_variable(self._confirm_var)
        return bool(self._confirm_result)

    def _resolve_confirm(self, value: bool) -> None:
        self._confirm_result = value
        self.confirm_frame.pack_forget()
        self._confirm_var.set(1)

    def _build_toast(self) -> None:
        self.toast_label = tk.Label(self.root, text="", bg=PALETTE["accent_deep"],
                                    fg="#ffffff", font=FONT_UI_B,
                                    padx=16, pady=10)

    def toast(self, text: str, level: str = "info", ms: int = 3000) -> None:
        bg = {"ok": "#124a2b", "warn": "#5a3a0c",
              "err": "#5a1325", "info": PALETTE["accent_deep"]}.get(
                  level, PALETTE["accent_deep"])
        self.toast_label.config(text=text, bg=bg)
        self.toast_label.place(relx=0.5, rely=0.06, anchor="n")
        if self._toast_after:
            self.root.after_cancel(self._toast_after)
        self._toast_after = self.root.after(ms, self.toast_label.place_forget)

    def _build_log_panel(self, parent: tk.Frame) -> None:
        outer = tk.Frame(parent, bg=PALETTE["bg_mid"])
        outer.pack(fill=tk.X, padx=22, pady=(0, 12))

        header = tk.Frame(outer, bg=PALETTE["bg_mid"])
        header.pack(fill=tk.X)
        tk.Label(header, text="Журнал", bg=PALETTE["bg_mid"],
                 fg=PALETTE["fg_dim"], font=FONT_UI_B).pack(side=tk.LEFT)
        ttk.Button(header, text="Сохранить", style="Ghost.TButton",
                   command=self.save_log).pack(side=tk.RIGHT, padx=3)
        ttk.Button(header, text="Очистить", style="Ghost.TButton",
                   command=self.clear_log).pack(side=tk.RIGHT, padx=3)

        self.txt_log = scrolledtext.ScrolledText(
            outer, height=8, state="disabled", font=FONT_MONO,
            bg=PALETTE["bg_glass"], fg=PALETTE["fg"],
            insertbackground=PALETTE["accent"],
            relief="flat", borderwidth=0, wrap="word")
        self.txt_log.pack(fill=tk.X, pady=(6, 0))
        self.txt_log.tag_config("ok", foreground=PALETTE["success"])
        self.txt_log.tag_config("warn", foreground=PALETTE["warning"])
        self.txt_log.tag_config("err", foreground=PALETTE["error"])
        self.txt_log.tag_config("info", foreground=PALETTE["fg"])

        self.log_menu = tk.Menu(self.root, tearoff=0,
                                bg=PALETTE["bg_card"], fg=PALETTE["fg"],
                                activebackground=PALETTE["accent_deep"],
                                activeforeground="#ffffff")
        self.log_menu.add_command(label="Копировать всё", command=self._copy_log)
        self.log_menu.add_command(label="Сохранить как…", command=self.save_log)
        self.log_menu.add_separator()
        self.log_menu.add_command(label="Очистить", command=self.clear_log)
        self.txt_log.bind("<Button-3>", self._show_log_menu)

    def _show_log_menu(self, event: tk.Event) -> None:
        try:
            self.log_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.log_menu.grab_release()

    def _copy_log(self) -> None:
        self.root.clipboard_clear()
        self.root.clipboard_append(self.txt_log.get("1.0", tk.END))
        self.toast("Лог скопирован", "ok")

    def log(self, message: str, level: str = "info") -> None:
        try:
            ts = datetime.datetime.now().strftime("%H:%M:%S")
            prefix = {"info": "·", "ok": "+", "warn": "!", "err": "×"}.get(level, "·")
            self.txt_log.configure(state="normal")
            self.txt_log.insert(tk.END, f"[{ts}] {prefix} {message}\n", level)
            self.txt_log.see(tk.END)
            self.txt_log.configure(state="disabled")
        except Exception:
            pass

    def clear_log(self) -> None:
        self.txt_log.configure(state="normal")
        self.txt_log.delete("1.0", tk.END)
        self.txt_log.configure(state="disabled")
        self.log("Журнал очищен.")

    def save_log(self) -> None:
        path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Текстовые файлы", "*.txt"), ("Все файлы", "*.*")],
            initialfile=f"zabhelper_log_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt")
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.txt_log.get("1.0", tk.END))
            self.toast("Лог сохранён", "ok")
        except Exception as e:
            self.log(f"Не удалось сохранить лог: {e}", "err")

    # ------------------------------------------------------------------ #
    #  НАВИГАЦИЯ
    # ------------------------------------------------------------------ #
    def show_section(self, key: str) -> None:
        if key not in self._section_frames:
            return
        self._current_section = key
        for k, frame in self._section_frames.items():
            if k == key:
                frame.pack(fill=tk.BOTH, expand=True)
            else:
                frame.pack_forget()

        meta = next((s for s in self.SECTIONS if s[0] == key), None)
        if meta:
            self.section_title.config(text=meta[2])

        subtitles = {
            "monitor":  "Процессы, включая скрытые и системные.",
            "running":  "Программы с окнами и фоновые процессы.",
            "services": "Службы и драйверы.",
            "startup":  "Записи автозагрузки.",
            "tweaks":   "Очистка и твики.",
            "repair":   "Восстановление компонентов системы.",
            "network":  "Диагностика и сброс сети.",
            "security": "Defender, UAC, брандмауэр.",
            "restrict": "Сканирование параметров и ручное редактирование значений.",
            "open":     "Быстрый доступ к оснасткам.",
            "backups":  "Бэкапы и точки восстановления.",
            "diag":     "Диагностический отчёт.",
        }
        self.section_subtitle.config(text=subtitles.get(key, ""))

        for k, item in self._section_buttons.items():
            active = (k == key)
            item.configure(bg=PALETTE["bg_card"] if active else PALETTE["bg_sidebar"])
            for child in item.winfo_children():
                child.configure(bg=PALETTE["bg_card"] if active else PALETTE["bg_sidebar"])
                if isinstance(child, tk.Label):
                    is_icon = child.cget("text") in [s[1] for s in self.SECTIONS]
                    if active:
                        child.configure(fg=PALETTE["accent_hi"] if is_icon else PALETTE["fg"])
                    else:
                        child.configure(fg=PALETTE["fg_dim"])

        if key == "monitor" and not self._proc_cache:
            self.refresh_processes()
        elif key == "running" and not self._running_cache:
            self.refresh_running(only_gui=True)
        elif key == "startup" and not self._startup_cache:
            self.refresh_startup()
        elif key == "restrict" and not self._restrict_cache:
            self.scan_restrictions()

    def _quick_search(self) -> None:
        q = self.search_var.get().strip().lower()
        if not q:
            return
        self.log(f"Поиск: '{q}'", "info")
        found = 0
        for sec_key, _, sec_title in self.SECTIONS:
            frame = self._section_frames.get(sec_key)
            if frame is None:
                continue
            for sub in self._iter_widgets(frame):
                if isinstance(sub, ttk.Button):
                    try:
                        if q in str(sub.cget("text")).lower():
                            self.log(f"  [{sec_title}] {sub.cget('text')}", "ok")
                            found += 1
                    except Exception:
                        pass
        if not found:
            self.log("Ничего не найдено.", "warn")

    def _iter_widgets(self, w: tk.Misc):
        yield w
        for c in w.winfo_children():
            yield from self._iter_widgets(c)

    # ------------------------------------------------------------------ #
    #  ХЕЛПЕРЫ UI
    # ------------------------------------------------------------------ #
    def _make_card(self, parent: tk.Misc, title: str, subtitle: str = "") -> tk.Frame:
        card = tk.Frame(parent, bg=PALETTE["bg_card"], highlightthickness=1,
                        highlightbackground=PALETTE["border"])
        card.pack(fill=tk.X, pady=6)
        head = tk.Frame(card, bg=PALETTE["bg_card"])
        head.pack(fill=tk.X, padx=16, pady=(12, 4))
        tk.Label(head, text=title, bg=PALETTE["bg_card"], fg=PALETTE["fg"],
                 font=FONT_UI_B).pack(anchor="w")
        if subtitle:
            tk.Label(head, text=subtitle, bg=PALETTE["bg_card"],
                     fg=PALETTE["fg_dim"], font=FONT_SMALL,
                     wraplength=900, justify="left").pack(anchor="w", pady=(2, 0))
        body = tk.Frame(card, bg=PALETTE["bg_card"])
        body.pack(fill=tk.X, padx=16, pady=(4, 12))
        return body

    def _grid_buttons(self, parent: tk.Frame, funcs: list[Any], cols: int = 3) -> None:
        for i, item in enumerate(funcs):
            label, cmd, *rest = item
            style = rest[0] if rest else "TButton"
            r, c = divmod(i, cols)
            ttk.Button(parent, text=label, command=cmd, style=style
                       ).grid(row=r, column=c, padx=4, pady=4, sticky="ew")
        for c in range(cols):
            parent.columnconfigure(c, weight=1)

    # ================================================================== #
    #  РАЗДЕЛ: МОНИТОРИНГ
    # ================================================================== #
    def _build_section_monitor(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])

        toolbar = tk.Frame(frame, bg=PALETTE["bg_mid"])
        toolbar.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(toolbar, text="Обновить", style="Accent.TButton",
                   command=self.refresh_processes).pack(side=tk.LEFT)
        ttk.Button(toolbar, text="Автообновление",
                   command=self.toggle_auto_refresh).pack(side=tk.LEFT, padx=4)
        ttk.Button(toolbar, text="Дерево процессов",
                   command=self.show_process_tree).pack(side=tk.LEFT, padx=4)
        ttk.Button(toolbar, text="CSV", command=self.export_processes_csv
                   ).pack(side=tk.LEFT, padx=4)
        ttk.Button(toolbar, text="Завершить", style="Danger.TButton",
                   command=self.kill_selected_process).pack(side=tk.RIGHT, padx=4)
        ttk.Button(toolbar, text="Свойства",
                   command=self.show_selected_process_props).pack(side=tk.RIGHT, padx=4)
        ttk.Button(toolbar, text="Папка",
                   command=self.open_selected_process_folder).pack(side=tk.RIGHT, padx=4)

        flt = tk.Frame(frame, bg=PALETTE["bg_card"], highlightthickness=1,
                       highlightbackground=PALETTE["border"])
        flt.pack(fill=tk.X, pady=(0, 6))
        inner = tk.Frame(flt, bg=PALETTE["bg_card"])
        inner.pack(fill=tk.X, padx=14, pady=10)

        self.show_hidden_var  = tk.BooleanVar(value=True)
        self.show_system_var  = tk.BooleanVar(value=True)
        self.show_svchost_var = tk.BooleanVar(value=True)
        for text, var in [("Скрытые", self.show_hidden_var),
                          ("Системные", self.show_system_var),
                          ("svchost", self.show_svchost_var)]:
            ttk.Checkbutton(inner, text=text, variable=var,
                            command=self._apply_process_filter).pack(side=tk.LEFT, padx=(0, 10))
        tk.Label(inner, text="Фильтр:", bg=PALETTE["bg_card"],
                 fg=PALETTE["fg_dim"], font=FONT_SMALL).pack(side=tk.LEFT, padx=(10, 6))
        self.proc_filter = tk.StringVar()
        tk.Entry(inner, textvariable=self.proc_filter,
                 bg=PALETTE["bg_input"], fg=PALETTE["fg"],
                 insertbackground=PALETTE["accent"],
                 relief="flat", font=FONT_UI).pack(side=tk.LEFT, fill=tk.X,
                                                   expand=True, ipady=4, padx=4)
        self.proc_filter.trace_add("write", lambda *a: self._apply_process_filter())

        self.proc_stats = tk.Label(frame, text="", bg=PALETTE["bg_mid"],
                                   fg=PALETTE["fg_dim"], font=FONT_SMALL)
        self.proc_stats.pack(anchor="w", pady=(0, 4))

        cols = ("PID", "PPID", "Имя", "Пользователь", "CPU, с",
                "Память, МБ", "Потоки", "Статус", "Путь")
        self.proc_tree = ttk.Treeview(frame, columns=cols, show="headings")
        widths = {"PID": 70, "PPID": 70, "Имя": 220, "Пользователь": 170,
                  "CPU, с": 80, "Память, МБ": 100, "Потоки": 70,
                  "Статус": 130, "Путь": 520}
        for c in cols:
            self.proc_tree.heading(c, text=c,
                                   command=lambda col=c: self._sort_processes(col))
            self.proc_tree.column(c, width=widths[c],
                                  anchor=("e" if "МБ" in c or "CPU" in c else "w"))
        self.proc_tree.pack(fill=tk.BOTH, expand=True)
        self.proc_tree.tag_configure("system", foreground=PALETTE["sys"])
        self.proc_tree.tag_configure("hidden", foreground=PALETTE["hidden"])
        self.proc_tree.tag_configure("user", foreground=PALETTE["fg"])
        self.proc_tree.bind("<Button-3>", self._show_proc_menu)
        self.proc_tree.bind("<Double-1>", lambda e: self.show_selected_process_props())

        self.proc_menu = tk.Menu(self.root, tearoff=0,
                                 bg=PALETTE["bg_card"], fg=PALETTE["fg"],
                                 activebackground=PALETTE["accent_deep"],
                                 activeforeground="#ffffff")
        self.proc_menu.add_command(label="Завершить", command=self.kill_selected_process)
        self.proc_menu.add_command(label="Дерево", command=self.show_process_tree)
        self.proc_menu.add_separator()
        self.proc_menu.add_command(label="Открыть папку",
                                   command=self.open_selected_process_folder)
        self.proc_menu.add_command(label="Свойства",
                                   command=self.show_selected_process_props)
        self.proc_menu.add_command(label="Поиск в интернете",
                                   command=self.search_selected_process)
        return frame

    def _show_proc_menu(self, event: tk.Event) -> None:
        row = self.proc_tree.identify_row(event.y)
        if row:
            self.proc_tree.selection_set(row)
            try:
                self.proc_menu.tk_popup(event.x_root, event.y_root)
            finally:
                self.proc_menu.grab_release()

    def toggle_auto_refresh(self) -> None:
        self._auto_refresh = not self._auto_refresh
        if self._auto_refresh:
            self.toast("Автообновление включено (3 с)", "ok")
            self._auto_refresh_tick()
        else:
            self.toast("Автообновление выключено", "warn")
            if self._auto_refresh_after:
                self.root.after_cancel(self._auto_refresh_after)
                self._auto_refresh_after = None

    def _auto_refresh_tick(self) -> None:
        if not self._auto_refresh:
            return
        if self._current_section == "monitor":
            self.refresh_processes()
        self._auto_refresh_after = self.root.after(3000, self._auto_refresh_tick)

    def refresh_processes(self) -> None:
        self.log("Чтение процессов (WMI/CIM)…", "info")
        ps = r"""
$result = @()
$allProcs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue
foreach ($p in $allProcs) {
    $cpu = 0.0; $mem = 0.0; $threads = 0; $user = ""; $path = ""
    try {
        $wp = Get-Process -Id $p.ProcessId -ErrorAction SilentlyContinue
        if ($wp) {
            if ($wp.CPU) { $cpu = [math]::Round($wp.CPU, 2) }
            $mem = [math]::Round($wp.WorkingSet64 / 1MB, 2)
            $threads = $wp.Threads.Count
            $path = $wp.Path
        }
    } catch {}
    try {
        $owner = Invoke-CimMethod -InputObject $p -MethodName GetOwner -ErrorAction SilentlyContinue
        if ($owner -and $owner.User) { $user = "$($owner.Domain)\$($owner.User)" }
    } catch {}
    $result += [PSCustomObject]@{
        PID = $p.ProcessId; PPID = $p.ParentProcessId; Name = $p.Name
        User = $user; CPU = $cpu; MB = $mem; Threads = $threads
        Path = if ($path) { $path } else { $p.ExecutablePath }
        CommandLine = $p.CommandLine
    }
}
$result | ConvertTo-Json -Depth 3 -Compress
"""
        out = ConsoleRunner.run_ps(ps, timeout=90)
        data: list[dict[str, Any]] = []
        if out.strip().startswith(("[", "{")):
            try:
                parsed = json.loads(out)
                data = parsed if isinstance(parsed, list) else [parsed]
            except Exception as e:
                self.log(f"Ошибка JSON: {e}", "err")

        if not data:
            self.log("WMI недоступен — tasklist", "warn")
            out = ConsoleRunner.run("tasklist /fo csv /nh /v")
            tmp: list[dict[str, Any]] = []
            for line in out.splitlines():
                parts = [p.strip('"') for p in line.split('","')]
                if len(parts) >= 8:
                    tmp.append({
                        "pid": parts[1], "ppid": "?", "name": parts[0],
                        "user": parts[6], "cpu": 0,
                        "mb": parts[4].replace(" K", "").replace(",", "").strip(),
                        "threads": 0, "path": "", "cmd": "",
                        "status": self._process_status(0, parts[0], parts[6]),
                    })
            self._proc_cache = tmp
        else:
            cache: list[dict[str, Any]] = []
            for p in data:
                pid = str(p.get("PID", ""))
                ppid = str(p.get("PPID", ""))
                name = p.get("Name", "") or "?"
                user = p.get("User", "") or ""
                cpu = float(p.get("CPU", 0) or 0)
                mb = float(p.get("MB", 0) or 0)
                threads = int(p.get("Threads", 0) or 0)
                path = p.get("Path", "") or ""
                cmd = p.get("CommandLine", "") or ""
                status = self._process_status(int(pid) if pid.isdigit() else 0, name, user)
                cache.append({"pid": pid, "ppid": ppid, "name": name, "user": user,
                              "cpu": cpu, "mb": mb, "threads": threads,
                              "path": path, "cmd": cmd, "status": status})
            self._proc_cache = cache

        self._apply_process_filter()
        self.log(f"Всего процессов: {len(self._proc_cache)}.", "ok")

    @staticmethod
    def _process_status(pid: int, name: str, user: str) -> str:
        if pid in (0, 4):
            return "system"
        n = name.lower()
        if n in ("system", "registry", "memory compression", "secure system",
                 "smss.exe", "csrss.exe", "wininit.exe", "winlogon.exe",
                 "services.exe", "lsass.exe", "fontdrvhost.exe", "dwm.exe"):
            return "system"
        if user.lower().startswith(("nt authority\\", "nt service\\",
                                    "font driver host\\")):
            return "system"
        if not user:
            return "hidden"
        return "user"

    def _apply_process_filter(self) -> None:
        if not hasattr(self, "proc_tree"):
            return
        q = self.proc_filter.get().strip().lower() if hasattr(self, "proc_filter") else ""
        for row in self.proc_tree.get_children():
            self.proc_tree.delete(row)
        shown = sys_cnt = hid_cnt = usr_cnt = 0
        for p in self._proc_cache:
            st = p.get("status", "user")
            if st == "system":
                sys_cnt += 1
                if not self.show_system_var.get():
                    continue
            elif st == "hidden":
                hid_cnt += 1
                if not self.show_hidden_var.get():
                    continue
            else:
                usr_cnt += 1
            if p["name"].lower() == "svchost.exe" and not self.show_svchost_var.get():
                continue
            if q and (q not in p["name"].lower()
                      and q not in str(p["path"]).lower()
                      and q not in str(p["user"]).lower()
                      and q not in str(p["pid"])):
                continue
            self.proc_tree.insert("", "end",
                values=(p["pid"], p["ppid"], p["name"], p["user"], p["cpu"],
                        p["mb"], p["threads"],
                        {"system": "СИСТЕМНЫЙ", "hidden": "СКРЫТЫЙ",
                         "user": "пользователь"}.get(st, st),
                        p["path"]),
                tags=(st,))
            shown += 1
        self.proc_stats.config(
            text=f"Показано: {shown}  ·  всего: {len(self._proc_cache)}  ·  "
                 f"пользовательских: {usr_cnt}  ·  системных: {sys_cnt}  ·  "
                 f"скрытых: {hid_cnt}  ·  "
                 f"права: {'админ' if self.is_admin else 'без админа'}")

    def _sort_processes(self, col: str) -> None:
        if self._proc_sort_col == col:
            self._proc_sort_rev = not self._proc_sort_rev
        else:
            self._proc_sort_col = col
            self._proc_sort_rev = False

        def to_int(v: Any) -> int:
            try:
                return int(v)
            except (TypeError, ValueError):
                return 0

        def to_float(v: Any) -> float:
            try:
                return float(v)
            except (TypeError, ValueError):
                return 0.0

        key_map: dict[str, Callable[[dict[str, Any]], Any]] = {
            "PID": lambda p: to_int(p["pid"]),
            "PPID": lambda p: to_int(p["ppid"]),
            "Имя": lambda p: str(p["name"]).lower(),
            "Пользователь": lambda p: str(p["user"]).lower(),
            "CPU, с": lambda p: to_float(p["cpu"]),
            "Память, МБ": lambda p: to_float(p["mb"]),
            "Потоки": lambda p: to_int(p["threads"]),
            "Статус": lambda p: p["status"],
            "Путь": lambda p: str(p["path"]).lower(),
        }
        if col in key_map:
            self._proc_cache.sort(key=key_map[col], reverse=self._proc_sort_rev)
        self._apply_process_filter()

    def show_process_tree(self) -> None:
        win = tk.Toplevel(self.root)
        win.title("Дерево процессов")
        win.geometry("1000x640")
        win.configure(bg=PALETTE["bg_mid"])
        tk.Label(win, text="Дерево процессов", bg=PALETTE["bg_mid"],
                 fg=PALETTE["accent_hi"], font=FONT_H2).pack(pady=10)
        tree = ttk.Treeview(win, columns=("PID", "MB", "Path"), show="tree headings")
        tree.heading("#0", text="Процесс")
        tree.heading("PID", text="PID")
        tree.heading("MB", text="МБ")
        tree.heading("Path", text="Путь")
        tree.column("#0", width=380)
        tree.column("PID", width=70, anchor="center")
        tree.column("MB", width=90, anchor="e")
        tree.column("Path", width=420)
        tree.pack(fill=tk.BOTH, expand=True, padx=12, pady=10)

        by_pid = {p["pid"]: p for p in self._proc_cache}
        children: dict[str, list[dict[str, Any]]] = {}
        for p in self._proc_cache:
            children.setdefault(p.get("ppid", "0"), []).append(p)

        def add_node(parent_node: str, proc: dict[str, Any]) -> None:
            node = tree.insert(parent_node, "end",
                               text=f"{proc['name']}  ({proc['user'] or '—'})",
                               values=(proc["pid"], proc["mb"], proc["path"]),
                               tags=(proc.get("status", "user"),))
            for child in children.get(proc["pid"], []):
                add_node(node, child)

        for pid, procs in children.items():
            if pid not in by_pid and pid != "0":
                for proc in procs:
                    add_node("", proc)
        if not tree.get_children():
            for p in self._proc_cache:
                add_node("", p)
        tree.tag_configure("system", foreground=PALETTE["sys"])
        tree.tag_configure("hidden", foreground=PALETTE["hidden"])
        ttk.Button(win, text="Закрыть", command=win.destroy).pack(pady=6)

    def _get_selected_process(self) -> Optional[dict[str, Any]]:
        sel = self.proc_tree.selection()
        if not sel:
            self.toast("Процесс не выбран", "warn")
            return None
        values = self.proc_tree.item(sel[0], "values")
        pid, name = values[0], values[2]
        for p in self._proc_cache:
            if p["pid"] == pid and p["name"] == name:
                return p
        return None

    def kill_selected_process(self) -> None:
        p = self._get_selected_process()
        if not p:
            return
        if p["pid"] in ("0", "4"):
            self.toast("Системный процесс PID=0/4 нельзя завершить", "err")
            return
        pid = str(p["pid"])
        try:
            result = subprocess.run(
                ["taskkill", "/f", "/t", "/pid", pid],
                capture_output=True, timeout=15,
                creationflags=ConsoleRunner.CREATE_NO_WINDOW,
            )
            out = (ConsoleRunner._decode(result.stdout or b"")
                   + ConsoleRunner._decode(result.stderr or b""))
            rc = result.returncode
        except Exception as e:
            self.log(f"taskkill PID={pid}: {e}", "err")
            self.toast(f"Не удалось завершить PID {pid}", "err")
            return
        if rc == 0:
            self.log(f"Процесс {p['name']} (PID {pid}) завершён.", "ok")
            self.toast(f"Завершён: {p['name']}", "ok")
        else:
            self.log(f"taskkill PID={pid} rc={rc}: {out.strip()[:300]}", "err")
            self.toast(f"Не удалось завершить {p['name']}", "err")
        self.refresh_processes()

    def open_selected_process_folder(self) -> None:
        p = self._get_selected_process()
        if not p:
            return
        path = p.get("path", "")
        if path and os.path.exists(path):
            subprocess.Popen(f'explorer /select,"{path}"', shell=True)
            self.toast("Папка открыта", "ok")
        else:
            self.toast("Путь неизвестен", "warn")

    def show_selected_process_props(self) -> None:
        p = self._get_selected_process()
        if not p:
            return
        path = p.get("path", "")
        if path and os.path.exists(path):
            try:
                ret = ctypes.windll.shell32.ShellExecuteW(
                    None, "properties", path, None, None, 1
                )
                if int(ret) > 32:
                    self.log(f"Свойства: {path}", "ok")
                    self.toast("Свойства открыты", "ok")
                else:
                    self.log(f"ShellExecuteW code={ret} для {path}", "err")
                    self.toast("Не удалось открыть свойства", "err")
            except Exception as e:
                self.log(f"Ошибка открытия свойств: {e}", "err")
                self.toast("Не удалось открыть свойства", "err")
        else:
            self.log(f"PID={p['pid']}, имя={p['name']}, "
                     f"пользователь={p['user'] or '—'}, "
                     f"память={p['mb']} МБ, статус={p['status']}", "info")
            self.toast(f"{p['name']} (PID {p['pid']})", "info")

    def search_selected_process(self) -> None:
        p = self._get_selected_process()
        if not p:
            return
        q = p["name"] or p["path"]
        subprocess.Popen(
            f'start "" "https://www.google.com/search?q={q}+Windows+process"',
            shell=True)
        self.toast(f"Поиск: {q}", "ok")

    def export_processes_csv(self) -> None:
        path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV", "*.csv"), ("Все", "*.*")],
            initialfile=f"processes_{datetime.datetime.now():%Y%m%d_%H%M%S}.csv")
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8-sig", newline="") as f:
                w = csv.writer(f, delimiter=";")
                w.writerow(["PID", "PPID", "Имя", "Пользователь", "CPU",
                            "МБ", "Потоки", "Статус", "Путь", "Командная строка"])
                for p in self._proc_cache:
                    w.writerow([p["pid"], p["ppid"], p["name"], p["user"],
                                p["cpu"], p["mb"], p["threads"], p["status"],
                                p["path"], p.get("cmd", "")])
            self.toast("CSV сохранён", "ok")
        except Exception as e:
            self.log(f"Ошибка экспорта: {e}", "err")

    # ================================================================== #
    #  РАЗДЕЛ: ЗАПУЩЕННЫЕ
    # ================================================================== #
    def _build_section_running(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        tb = tk.Frame(frame, bg=PALETTE["bg_mid"]); tb.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(tb, text="Обновить", style="Accent.TButton",
                   command=self.refresh_running).pack(side=tk.LEFT)
        ttk.Button(tb, text="С окнами",
                   command=lambda: self.refresh_running(only_gui=True)).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Все",
                   command=lambda: self.refresh_running(only_gui=False)).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Завершить", style="Danger.TButton",
                   command=self.kill_selected_running).pack(side=tk.RIGHT, padx=4)
        ttk.Button(tb, text="Папка",
                   command=self.open_selected_running_folder).pack(side=tk.RIGHT, padx=4)
        ttk.Button(tb, text="Свойства",
                   command=self.show_selected_running_props).pack(side=tk.RIGHT, padx=4)

        filt = tk.Frame(frame, bg=PALETTE["bg_card"], highlightthickness=1,
                        highlightbackground=PALETTE["border"])
        filt.pack(fill=tk.X, pady=(0, 6))
        inner = tk.Frame(filt, bg=PALETTE["bg_card"]); inner.pack(fill=tk.X, padx=14, pady=10)
        tk.Label(inner, text="Фильтр:", bg=PALETTE["bg_card"],
                 fg=PALETTE["fg_dim"], font=FONT_SMALL).pack(side=tk.LEFT, padx=(0, 6))
        self.running_filter = tk.StringVar()
        tk.Entry(inner, textvariable=self.running_filter, bg=PALETTE["bg_input"],
                 fg=PALETTE["fg"], insertbackground=PALETTE["accent"],
                 relief="flat", font=FONT_UI).pack(side=tk.LEFT, fill=tk.X,
                                                   expand=True, ipady=4)
        self.running_filter.trace_add("write", lambda *a: self._apply_running_filter())

        cols = ("PID", "Имя", "Заголовок окна", "Память, МБ", "Путь")
        self.running_tree = ttk.Treeview(frame, columns=cols, show="headings")
        for c in cols:
            self.running_tree.heading(c, text=c)
        self.running_tree.column("PID", width=70, anchor="center")
        self.running_tree.column("Имя", width=200)
        self.running_tree.column("Заголовок окна", width=320)
        self.running_tree.column("Память, МБ", width=100, anchor="e")
        self.running_tree.column("Путь", width=520)
        self.running_tree.pack(fill=tk.BOTH, expand=True)
        self.running_tree.bind("<Double-1>", lambda e: self.show_selected_running_props())
        return frame

    def refresh_running(self, only_gui: bool = True) -> None:
        if not hasattr(self, "running_tree"):
            return
        for row in self.running_tree.get_children():
            self.running_tree.delete(row)
        self._running_cache = []
        self.log("Чтение запущенных программ…", "info")
        if only_gui:
            ps = ("Get-Process | Where-Object {$_.MainWindowTitle -ne ''} "
                  "| Select-Object Id,ProcessName,MainWindowTitle,"
                  "@{n='MB';e={[math]::Round($_.WS/1MB,1)}},Path "
                  "| ConvertTo-Json -Depth 2")
        else:
            ps = ("Get-Process | Select-Object Id,ProcessName,MainWindowTitle,"
                  "@{n='MB';e={[math]::Round($_.WS/1MB,1)}},Path "
                  "| ConvertTo-Json -Depth 2")
        out = ConsoleRunner.run_ps(ps, timeout=60)
        data: list[dict[str, Any]] = []
        if out.strip().startswith(("[", "{")):
            try:
                parsed = json.loads(out)
                data = parsed if isinstance(parsed, list) else [parsed]
            except Exception:
                data = []
        if not data:
            self.log("PowerShell недоступен — tasklist", "warn")
            out = ConsoleRunner.run("tasklist /fo csv /nh")
            for line in out.splitlines():
                parts = [p.strip('"') for p in line.split('","')]
                if len(parts) >= 5:
                    self._running_cache.append({
                        "pid": parts[1], "name": parts[0],
                        "title": parts[4] if parts[4] != "N/A" else "",
                        "mb": parts[3].replace(" K", "").replace(",", "").strip() or "?",
                        "path": "",
                    })
        else:
            for p in data:
                self._running_cache.append({
                    "pid": str(p.get("Id", "")),
                    "name": (p.get("ProcessName", "") + ".exe")
                            if p.get("ProcessName") else "",
                    "title": p.get("MainWindowTitle", "") or "",
                    "mb": str(p.get("MB", "")),
                    "path": p.get("Path", "") or "",
                })
        self._apply_running_filter()
        self.log(f"Найдено: {len(self._running_cache)}.", "ok")

    def _apply_running_filter(self) -> None:
        if not hasattr(self, "running_tree"):
            return
        q = self.running_filter.get().strip().lower() if hasattr(self, "running_filter") else ""
        for row in self.running_tree.get_children():
            self.running_tree.delete(row)
        for it in self._running_cache:
            if q and (q not in it["name"].lower()
                      and q not in it["title"].lower()
                      and q not in it["path"].lower()):
                continue
            self.running_tree.insert("", "end",
                values=(it["pid"], it["name"], it["title"], it["mb"], it["path"]))

    def _get_selected_running(self) -> Optional[dict[str, Any]]:
        sel = self.running_tree.selection()
        if not sel:
            self.toast("Не выбрано", "warn")
            return None
        values = self.running_tree.item(sel[0], "values")
        pid, name = values[0], values[1]
        for it in self._running_cache:
            if it["pid"] == pid and it["name"] == name:
                return it
        return None

    def kill_selected_running(self) -> None:
        it = self._get_selected_running()
        if not it:
            return
        pid = str(it["pid"])
        try:
            result = subprocess.run(
                ["taskkill", "/f", "/t", "/pid", pid],
                capture_output=True, timeout=15,
                creationflags=ConsoleRunner.CREATE_NO_WINDOW,
            )
            out = (ConsoleRunner._decode(result.stdout or b"")
                   + ConsoleRunner._decode(result.stderr or b""))
            rc = result.returncode
        except Exception as e:
            self.log(f"taskkill PID={pid}: {e}", "err")
            self.toast(f"Не удалось завершить PID {pid}", "err")
            return
        if rc == 0:
            self.log(f"Программа {it['name']} (PID {pid}) завершена.", "ok")
            self.toast(f"Завершено: {it['name']}", "ok")
        else:
            self.log(f"taskkill PID={pid} rc={rc}: {out.strip()[:300]}", "err")
            self.toast(f"Не удалось завершить {it['name']}", "err")
        self.refresh_running()

    def open_selected_running_folder(self) -> None:
        it = self._get_selected_running()
        if not it:
            return
        if it["path"] and os.path.exists(it["path"]):
            subprocess.Popen(f'explorer /select,"{it["path"]}"', shell=True)
            self.toast("Папка открыта", "ok")
        else:
            self.toast("Путь неизвестен", "warn")

    def show_selected_running_props(self) -> None:
        it = self._get_selected_running()
        if not it:
            return
        path = it.get("path", "")
        if path and os.path.exists(path):
            try:
                ret = ctypes.windll.shell32.ShellExecuteW(
                    None, "properties", path, None, None, 1
                )
                if int(ret) > 32:
                    self.log(f"Свойства: {path}", "ok")
                    self.toast("Свойства открыты", "ok")
                else:
                    self.log(f"ShellExecuteW code={ret} для {path}", "err")
                    self.toast("Не удалось открыть свойства", "err")
            except Exception as e:
                self.log(f"Ошибка открытия свойств: {e}", "err")
        else:
            self.toast(f"{it['name']} (PID {it['pid']})", "info")

    # ================================================================== #
    #  РАЗДЕЛ: СЛУЖБЫ
    # ================================================================== #
    def _build_section_services(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Службы и драйверы",
                               "Просмотр, запуск, остановка, конфигурирование.")
        funcs = [
            ("Список служб", self.list_services),
            ("Список драйверов", self.list_drivers),
            ("Разблокировать Windows Update", self.unlock_wuauserv),
            ("Восстановить Defender", self.restore_defender),
            ("Включить VSS", lambda: self.set_service_start("VSS", "demand")),
            ("Включить BITS", lambda: self.set_service_start("BITS", "delayed-auto")),
            ("Включить wuauserv", lambda: self.set_service_start("wuauserv", "demand")),
            ("Очистить IFEO", self.clear_ifeo),
            ("Снять запрет .msi",
             lambda: self.reg_reset(r"Software\Policies\Microsoft\Windows\Installer",
                                    "DisableMSI")),
            ("Открыть taskmgr", lambda: self.open_exe("taskmgr.exe")),
            ("Открыть Process Explorer", self.open_in_procexp),
            ("Открыть services.msc", lambda: self.open_exe("services.msc")),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    def open_in_procexp(self) -> None:
        candidates = [
            r"C:\Program Files\SysinternalsSuite\procexp64.exe",
            r"C:\Program Files\SysinternalsSuite\procexp.exe",
            r"C:\Program Files (x86)\SysinternalsSuite\procexp64.exe",
            r"C:\Program Files (x86)\SysinternalsSuite\procexp.exe",
            r"C:\SysinternalsSuite\procexp64.exe",
            r"C:\SysinternalsSuite\procexp.exe",
            os.path.join(os.path.expanduser("~"),
                         r"Downloads\SysinternalsSuite\procexp64.exe"),
        ]
        for c in candidates:
            if os.path.exists(c):
                try:
                    subprocess.Popen(f'"{c}"', shell=True)
                    self.log(f"Запущен Process Explorer: {c}", "ok")
                    self.toast("Process Explorer запущен", "ok")
                    return
                except Exception as e:
                    self.log(f"Не удалось запустить {c}: {e}", "err")
        try:
            subprocess.Popen(
                'start "" "https://learn.microsoft.com/sysinternals/downloads/process-explorer"',
                shell=True)
            self.log("Process Explorer не найден, открыта страница загрузки.", "warn")
            self.toast("Process Explorer не найден", "warn")
        except Exception as e:
            self.log(f"Не удалось открыть страницу: {e}", "err")

    # ================================================================== #
    #  РАЗДЕЛ: АВТОЗАГРУЗКА
    # ================================================================== #
    def _build_section_startup(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        tb = tk.Frame(frame, bg=PALETTE["bg_mid"]); tb.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(tb, text="Обновить", style="Accent.TButton",
                   command=self.refresh_startup).pack(side=tk.LEFT)
        ttk.Button(tb, text="Отключить",
                   command=self.disable_selected_startup).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Удалить", style="Danger.TButton",
                   command=self.delete_selected_startup).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Удалить сторонние", style="Danger.TButton",
                   command=self.delete_all_third_party).pack(side=tk.LEFT, padx=4)

        filt = tk.Frame(frame, bg=PALETTE["bg_card"], highlightthickness=1,
                        highlightbackground=PALETTE["border"])
        filt.pack(fill=tk.X, pady=(0, 6))
        inner = tk.Frame(filt, bg=PALETTE["bg_card"]); inner.pack(fill=tk.X, padx=14, pady=10)
        tk.Label(inner, text="Фильтр:", bg=PALETTE["bg_card"],
                 fg=PALETTE["fg_dim"], font=FONT_SMALL).pack(side=tk.LEFT, padx=(0, 6))
        self.startup_filter = tk.StringVar()
        tk.Entry(inner, textvariable=self.startup_filter, bg=PALETTE["bg_input"],
                 fg=PALETTE["fg"], insertbackground=PALETTE["accent"],
                 relief="flat", font=FONT_UI).pack(side=tk.LEFT, fill=tk.X,
                                                   expand=True, ipady=4)
        self.startup_filter.trace_add("write", lambda *a: self._apply_startup_filter())

        cols = ("Источник", "Имя", "Команда", "Статус")
        self.startup_tree = ttk.Treeview(frame, columns=cols, show="headings")
        for c in cols:
            self.startup_tree.heading(c, text=c)
        self.startup_tree.column("Источник", width=200)
        self.startup_tree.column("Имя", width=260)
        self.startup_tree.column("Команда", width=560)
        self.startup_tree.column("Статус", width=110, anchor="center")
        self.startup_tree.pack(fill=tk.BOTH, expand=True)
        self.startup_tree.bind("<Double-1>",
                               lambda e: self.open_selected_startup_location())
        return frame

    def refresh_startup(self) -> None:
        if winreg is None:
            self.log("winreg недоступен (не Windows?)", "err")
            return
        if not hasattr(self, "startup_tree"):
            return
        for row in self.startup_tree.get_children():
            self.startup_tree.delete(row)
        self._startup_cache = []

        locations = [
            (winreg.HKEY_CURRENT_USER,  r"Software\Microsoft\Windows\CurrentVersion\Run",      "HKCU\\Run"),
            (winreg.HKEY_CURRENT_USER,  r"Software\Microsoft\Windows\CurrentVersion\RunOnce",  "HKCU\\RunOnce"),
            (winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\Windows\CurrentVersion\Run",      "HKLM\\Run"),
            (winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\Windows\CurrentVersion\RunOnce",  "HKLM\\RunOnce"),
            (winreg.HKEY_LOCAL_MACHINE,
             r"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run",     "HKLM\\Wow\\Run"),
            (winreg.HKEY_LOCAL_MACHINE,
             r"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", "HKLM\\Wow\\RunOnce"),
        ]
        for hive, path, label in locations:
            try:
                key = winreg.OpenKey(hive, path)
                i = 0
                while True:
                    try:
                        name, val, _ = winreg.EnumValue(key, i)
                        self._startup_cache.append({
                            "source": label, "name": name, "cmd": val,
                            "type": "registry", "hive": hive, "path": path,
                            "status": "enabled",
                        })
                        i += 1
                    except OSError:
                        break
                winreg.CloseKey(key)
            except FileNotFoundError:
                continue
            except Exception as e:
                self.log(f"Ошибка чтения {label}: {e}", "warn")

        for folder, label in [
            (os.path.join(os.environ.get("APPDATA", ""),
                          r"Microsoft\Windows\Start Menu\Programs\Startup"),
             "Startup (User)"),
            (os.path.join(os.environ.get("ProgramData", ""),
                          r"Microsoft\Windows\Start Menu\Programs\Startup"),
             "Startup (Common)"),
        ]:
            if os.path.isdir(folder):
                for f in os.listdir(folder):
                    full = os.path.join(folder, f)
                    self._startup_cache.append({
                        "source": label, "name": f, "cmd": full,
                        "type": "folder", "path": full, "status": "enabled",
                    })

        self._apply_startup_filter()
        self.log(f"Автозагрузка: {len(self._startup_cache)} записей.", "ok")

    def _apply_startup_filter(self) -> None:
        if not hasattr(self, "startup_tree"):
            return
        q = self.startup_filter.get().strip().lower() if hasattr(self, "startup_filter") else ""
        for row in self.startup_tree.get_children():
            self.startup_tree.delete(row)
        for it in self._startup_cache:
            if q and (q not in it["name"].lower()
                      and q not in str(it["cmd"]).lower()
                      and q not in it["source"].lower()):
                continue
            self.startup_tree.insert("", "end",
                values=(it["source"], it["name"],
                        str(it["cmd"])[:200], it.get("status", "enabled")))

    def _get_selected_startup(self) -> Optional[dict[str, Any]]:
        sel = self.startup_tree.selection()
        if not sel:
            self.toast("Не выбрано", "warn")
            return None
        values = self.startup_tree.item(sel[0], "values")
        source, name = values[0], values[1]
        for it in self._startup_cache:
            if it["source"] == source and it["name"] == name:
                return it
        return None

    def disable_selected_startup(self) -> None:
        it = self._get_selected_startup()
        if not it or winreg is None:
            return
        try:
            if it["type"] == "registry":
                key = winreg.OpenKey(it["hive"], it["path"], 0, winreg.KEY_SET_VALUE)
                winreg.DeleteValue(key, it["name"])
                winreg.CloseKey(key)
            elif it["type"] == "folder":
                if not it["path"].endswith(".disabled"):
                    os.rename(it["path"], it["path"] + ".disabled")
            self.log(f"Отключено: {it['name']}", "ok")
            self.toast(f"Отключено: {it['name']}", "ok")
            self.refresh_startup()
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def delete_selected_startup(self) -> None:
        it = self._get_selected_startup()
        if not it or winreg is None:
            return
        try:
            if it["type"] == "registry":
                key = winreg.OpenKey(it["hive"], it["path"], 0, winreg.KEY_SET_VALUE)
                winreg.DeleteValue(key, it["name"])
                winreg.CloseKey(key)
            elif it["type"] == "folder":
                if os.path.isfile(it["path"]):
                    os.remove(it["path"])
                else:
                    shutil.rmtree(it["path"], ignore_errors=True)
            self.log(f"Удалено: {it['name']}", "ok")
            self.toast(f"Удалено: {it['name']}", "ok")
            self.refresh_startup()
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def delete_all_third_party(self) -> None:
        if winreg is None:
            return
        wl = {"SecurityHealth", "WindowsDefender", "OneDrive",
              "MicrosoftEdgeAutoLaunch", "RTHDVCPL"}
        cand = [it for it in self._startup_cache
                if it["type"] in ("registry", "folder")
                and it["name"] not in wl
                and not it["name"].lower().startswith(("windows", "microsoft"))]
        if not cand:
            self.toast("Сторонних записей нет", "warn")
            return
        removed = 0
        for it in cand:
            try:
                if it["type"] == "registry":
                    key = winreg.OpenKey(it["hive"], it["path"], 0, winreg.KEY_SET_VALUE)
                    winreg.DeleteValue(key, it["name"])
                    winreg.CloseKey(key)
                elif it["type"] == "folder":
                    if os.path.isfile(it["path"]):
                        os.remove(it["path"])
                    else:
                        shutil.rmtree(it["path"], ignore_errors=True)
                removed += 1
            except Exception as e:
                self.log(f"Ошибка {it['name']}: {e}", "err")
        self.log(f"Удалено сторонних записей: {removed}.", "ok")
        self.toast(f"Удалено: {removed}", "ok")
        self.refresh_startup()

    def open_selected_startup_location(self) -> None:
        it = self._get_selected_startup()
        if not it:
            return
        if it["type"] == "folder":
            subprocess.Popen(f'explorer /select,"{it["path"]}"', shell=True)
        elif it["type"] == "registry":
            self.open_exe("regedit.exe")
        else:
            self.open_exe("taskschd.msc")

    # ================================================================== #
    #  РАЗДЕЛ: ТВИКИ
    # ================================================================== #
    def _build_section_tweaks(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Твики, очистка и вызовы",
                               "Операции с подтверждением.")
        funcs = [
            ("Сбросить ассоциации .exe", self.reset_exe_assoc),
            ("Отключить залипание клавиш", self.disable_sticky_keys),
            ("Показать скрытые файлы", self.show_hidden_files),
            ("Показать расширения", self.show_extensions),
            ("Сбросить настройки проводника", self.reset_explorer_view),
            ("Сбросить hosts", self.reset_hosts),
            ("Очистить Temp", self.clean_user_temp),
            ("Очистить кэш обновлений", self.clean_wu_cache),
            ("Очистить Prefetch", self.clean_prefetch),
            ("Очистить корзину", self.clean_recycle_bin),
            ("Очистить DNS-кэш", self.flush_dns),
            ("Перезапустить explorer.exe", self.restart_explorer),
            ("sfc /scannow", lambda: self.run_cmd_async("sfc /scannow")),
            ("DISM /RestoreHealth",
             lambda: self.run_cmd_async("DISM /Online /Cleanup-Image /RestoreHealth")),
            ("chkdsk", lambda: self.run_cmd_async("chkdsk")),
            ("CMD на Рабочем столе", self.open_cmd_desktop),
            ("PowerShell от админа", lambda: self.open_exe("powershell.exe")),
            ("Перезагрузка через 60 сек", self.confirm_reboot, "Danger.TButton"),
            ("Отмена перезагрузки", lambda: self.run_quiet("shutdown /a")),
            ("Включить редактор реестра",
             lambda: self.reg_reset(
                 r"Software\Microsoft\Windows\CurrentVersion\Policies\System",
                 "DisableRegistryTools")),
            ("Включить диспетчер задач",
             lambda: self.reg_reset(
                 r"Software\Microsoft\Windows\CurrentVersion\Policies\System",
                 "DisableTaskMgr")),
            ("Включить командную строку",
             lambda: self.reg_reset(
                 r"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer",
                 "DisableCMD")),
            ("Разрешить панель управления",
             lambda: self.reg_reset(
                 r"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer",
                 "NoControlPanel")),
            ("Восстановить msconfig", self.restore_msconfig),
            ("Восстановить PowerShell", self.restore_powershell),
            ("Разблокировать gpedit", self.restore_gpedit),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    # ================================================================== #
    #  РАЗДЕЛ: ВОССТАНОВЛЕНИЕ
    # ================================================================== #
    def _build_section_repair(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Ремонт и восстановление",
                               "Компоненты Windows, сеть, WMI, брандмауэр.")
        funcs = [
            ("sfc /scannow", lambda: self.run_cmd_async("sfc /scannow")),
            ("DISM RestoreHealth",
             lambda: self.run_cmd_async("DISM /Online /Cleanup-Image /RestoreHealth")),
            ("Сброс Winsock и TCP/IP", self.repair_network),
            ("Сброс Windows Update", self.repair_windows_update),
            ("Сброс кэша Store", self.repair_store),
            ("Восстановление шрифтов", self.repair_fonts),
            ("Перезапуск планировщика", self.repair_scheduler),
            ("Пересборка WMI", self.repair_wmi),
            ("Сброс брандмауэра", self.repair_firewall),
            ("Включение .NET 3.5",
             lambda: self.run_cmd_async(
                 "dism /online /enable-feature /featurename:NetFx3 /all")),
            ("rstrui", lambda: self.open_exe("rstrui.exe")),
            ("systemreset", lambda: self.open_exe("systemreset.exe")),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    # ================================================================== #
    #  РАЗДЕЛ: СЕТЬ
    # ================================================================== #
    def _build_section_network(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        top_body = self._make_card(frame, "Диагностика сети",
                                   "ping / tracert / nslookup")
        top = tk.Frame(top_body, bg=PALETTE["bg_card"]); top.pack(fill=tk.X)
        tk.Label(top, text="Хост:", bg=PALETTE["bg_card"],
                 fg=PALETTE["fg_dim"], font=FONT_SMALL).pack(side=tk.LEFT)
        self.net_host = tk.Entry(top, bg=PALETTE["bg_input"], fg=PALETTE["fg"],
                                 insertbackground=PALETTE["accent"], relief="flat",
                                 font=("Consolas", 10), width=30)
        self.net_host.pack(side=tk.LEFT, padx=6, ipady=4)
        self.net_host.insert(0, "8.8.8.8")
        ttk.Button(top, text="Ping", command=self.do_ping).pack(side=tk.LEFT, padx=3)
        ttk.Button(top, text="Tracert", command=self.do_tracert).pack(side=tk.LEFT, padx=3)
        ttk.Button(top, text="nslookup", command=self.do_nslookup).pack(side=tk.LEFT, padx=3)

        body = self._make_card(frame, "Сетевые операции", "")
        funcs = [
            ("IP-конфигурация", lambda: self._show_output("ipconfig /all")),
            ("Маршруты", lambda: self._show_output("route print")),
            ("ARP", lambda: self._show_output("arp -a")),
            ("DNS-кэш", lambda: self._show_output("ipconfig /displaydns")),
            ("Соединения", lambda: self._show_output("netstat -ano")),
            ("Сброс Winsock", self.reset_winsock),
            ("Сброс TCP/IP", self.reset_tcpip),
            ("Сброс DNS", self.flush_dns),
            ("Сброс прокси", self.reset_proxy),
            ("Wi-Fi профили", lambda: self._show_output("netsh wlan show profiles")),
            ("Сетевые подключения", lambda: self.open_exe("ncpa.cpl")),
            ("Брандмауэр", lambda: self.open_exe("wf.msc")),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    def _show_output(self, command: str) -> None:
        out = ConsoleRunner.run(command)
        self.log(f"--- {command} ---", "info")
        for line in out.splitlines()[:30]:
            if line.strip():
                self.log(line)
        self.log(f"(всего строк: {len(out.splitlines())})", "info")

    def do_ping(self) -> None:
        host = self.net_host.get().strip() or "8.8.8.8"
        self.log(f"Ping {host}…", "info")
        def run() -> None:
            out = ConsoleRunner.run(f"ping -n 4 {host}")
            for line in out.splitlines():
                self.log(line)
        threading.Thread(target=run, daemon=True).start()

    def do_tracert(self) -> None:
        host = self.net_host.get().strip() or "8.8.8.8"
        self.log(f"Tracert {host}…", "info")
        def run() -> None:
            out = ConsoleRunner.run(f"tracert -h 15 -w 500 {host}", timeout=180)
            for line in out.splitlines():
                self.log(line)
        threading.Thread(target=run, daemon=True).start()

    def do_nslookup(self) -> None:
        host = self.net_host.get().strip() or "google.com"
        out = ConsoleRunner.run(f"nslookup {host}")
        for line in out.splitlines():
            self.log(line)

    # ================================================================== #
    #  РАЗДЕЛ: БЕЗОПАСНОСТЬ
    # ================================================================== #
    def _build_section_security(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Безопасность",
                               "Defender, UAC, брандмауэр, аудит.")
        funcs = [
            ("Статус Defender", self.check_defender_status),
            ("Статус брандмауэра", self.check_firewall_status),
            ("Статус UAC", self.check_uac_status),
            ("Последние угрозы", self.check_defender_threats),
            ("Быстрая проверка", self.run_defender_quick, "Accent.TButton"),
            ("Полная проверка", self.run_defender_full, "Accent.TButton"),
            ("Обновить базы", self.update_defender),
            ("Восстановить Defender", self.restore_defender),
            ("Включить брандмауэр", self.enable_firewall),
            ("Включить UAC", self.enable_uac),
            ("Администраторы", self.list_admins),
            ("Сессии", lambda: self._show_output("query user")),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    # ================================================================== #
    #  РАЗДЕЛ: ОГРАНИЧЕНИЯ (только чтение + ручное редактирование значения)
    # ================================================================== #
    def _build_section_restrictions(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])

        tb = tk.Frame(frame, bg=PALETTE["bg_mid"]); tb.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(tb, text="Сканировать", style="Accent.TButton",
                   command=self.scan_restrictions).pack(side=tk.LEFT)
        ttk.Button(tb, text="Изменить значение", style="Success.TButton",
                   command=self.edit_selected_restriction).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Открыть ветку в regedit",
                   command=self.open_selected_restriction_in_regedit
                   ).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Папка бэкапов",
                   command=self.open_restrictions_backup_folder
                   ).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="CSV", command=self.export_restrictions_csv
                   ).pack(side=tk.RIGHT, padx=4)
        ttk.Button(tb, text="Копировать отчёт",
                   command=self.copy_restrictions_report
                   ).pack(side=tk.RIGHT, padx=4)

        info = tk.Frame(frame, bg=PALETTE["bg_card"], highlightthickness=1,
                        highlightbackground=PALETTE["border"])
        info.pack(fill=tk.X, pady=(0, 6))
        self.restrict_stats = tk.Label(info, text="Нажмите «Сканировать»…",
                                       bg=PALETTE["bg_card"], fg=PALETTE["fg_dim"],
                                       font=FONT_SMALL, anchor="w", justify="left")
        self.restrict_stats.pack(fill=tk.X, padx=14, pady=10)

        cols = ("Категория", "Расположение", "Параметр", "Значение", "Статус")
        self.restrict_tree = ttk.Treeview(frame, columns=cols, show="headings",
                                          selectmode="extended")
        headers = {
            "Категория":   (200, "w"),
            "Расположение":(320, "w"),
            "Параметр":    (240, "w"),
            "Значение":    (160, "center"),
            "Статус":      (200, "center"),
        }
        for c in cols:
            w, a = headers[c]
            self.restrict_tree.heading(c, text=c)
            self.restrict_tree.column(c, width=w, anchor=a)
        self.restrict_tree.pack(fill=tk.BOTH, expand=True)
        self.restrict_tree.tag_configure("found",  foreground=PALETTE["warning"])
        self.restrict_tree.tag_configure("ok",     foreground=PALETTE["success"])
        self.restrict_tree.tag_configure("locked", foreground=PALETTE["error"])
        self.restrict_tree.tag_configure("info",   foreground=PALETTE["fg_dim"])
        self.restrict_tree.bind("<Double-1>",
                                lambda e: self.edit_selected_restriction())
        self.restrict_tree.bind("<Button-3>", self._show_restrict_menu)

        self.restrict_menu = tk.Menu(self.root, tearoff=0,
                                     bg=PALETTE["bg_card"], fg=PALETTE["fg"],
                                     activebackground=PALETTE["accent_deep"],
                                     activeforeground="#ffffff")
        self.restrict_menu.add_command(label="Изменить значение…",
                                       command=self.edit_selected_restriction)
        self.restrict_menu.add_command(label="Открыть ветку в regedit",
                                       command=self.open_selected_restriction_in_regedit)
        self.restrict_menu.add_separator()
        self.restrict_menu.add_command(label="Копировать строку",
                                       command=self.copy_selected_restriction_row)

        return frame

    def _show_restrict_menu(self, event: tk.Event) -> None:
        row = self.restrict_tree.identify_row(event.y)
        if row:
            if row not in self.restrict_tree.selection():
                self.restrict_tree.selection_set(row)
            try:
                self.restrict_menu.tk_popup(event.x_root, event.y_root)
            finally:
                self.restrict_menu.grab_release()

    def _restriction_catalog(self) -> list[tuple[Any, ...]]:
        """Каталог параметров для сканирования. Только чтение.

        Формат: (категория, hive, subkey, value_name, bad_value).
        bad_value используется только чтобы вычислить статус.
        """
        if winreg is None:
            return []
        HKLM = winreg.HKEY_LOCAL_MACHINE
        HKCU = winreg.HKEY_CURRENT_USER
        return [
            ("Редактор реестра", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "DisableRegistryTools", 1),
            ("Редактор реестра", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "DisableRegistryTools", 1),
            ("Диспетчер задач", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "DisableTaskMgr", 1),
            ("Диспетчер задач", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "DisableTaskMgr", 1),
            ("Командная строка", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "DisableCMD", 1),
            ("Командная строка", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "DisableCMD", 2),
            ("Контекстное меню", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoViewContextMenu", 1),
            ("Свойства папки", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoFolderOptions", 1),
            ("Win+R", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoRun", 1),
            ("Панель управления", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoControlPanel", 1),
            ("Панель управления", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoControlPanel", 1),
            ("Значки рабочего стола", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoDesktop", 1),
            ("Смена обоев", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop",
             "NoChangingWallPaper", 1),
            ("Экран блокировки", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows\Personalization",
             "NoLockScreen", 1),
            ("Меню Пуск", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoStartMenuPinnedList", 1),
            ("Defender", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows Defender",
             "DisableAntiSpyware", 1),
            ("Defender", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows Defender",
             "DisableAntiVirus", 1),
            ("Defender: real-time", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection",
             "DisableRealtimeMonitoring", 1),
            ("Defender: cloud", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows Defender\Spynet",
             "SpynetReporting", 0),
            ("Windows Update", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU",
             "NoAutoUpdate", 1),
            ("Windows Update", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate",
             "DisableWindowsUpdateAccess", 1),
            ("Windows Update", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU",
             "AUOptions", 1),
            ("UAC", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "EnableLUA", 0),
            ("UAC", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
             "ConsentPromptBehaviorAdmin", 0),
            ("Установщик MSI", HKLM,
             r"SOFTWARE\Policies\Microsoft\Windows\Installer",
             "DisableMSI", 1),
            ("Запуск только разрешённых", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "RestrictRun", 1),
            ("Запуск только разрешённых", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "RestrictRun", 1),
            ("Windows Script Host", HKLM,
             r"SOFTWARE\Microsoft\Windows Script Host\Settings",
             "Enabled", 0),
            ("Autorun USB", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",
             "NoDriveTypeAutoRun", 0xFF),
            ("Прокси (HKCU)", HKCU,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings",
             "ProxyEnable", 1),
            ("Прокси (HKLM)", HKLM,
             r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings",
             "ProxyEnable", 1),
        ]

    def scan_restrictions(self) -> None:
        """Сканирование. ТОЛЬКО ЧТЕНИЕ."""
        if winreg is None:
            self.log("winreg недоступен — раздел не работает вне Windows.", "err")
            return
        if not hasattr(self, "restrict_tree"):
            return
        for row in self.restrict_tree.get_children():
            self.restrict_tree.delete(row)
        self._restrict_cache = []

        catalog = self._restriction_catalog()
        found = clean = missing = 0

        for cat, hive, subkey, value_name, bad_value in catalog:
            entry: dict[str, Any] = {
                "category": cat,
                "location": subkey,
                "value": value_name,
                "hive": hive,
                "hive_name": self._hive_name(hive),
                "bad_value": bad_value,
                "data": None,
                "vtype": None,
                "tag": "info",
                "status": "",
            }
            try:
                with winreg.OpenKey(hive, subkey, 0, winreg.KEY_READ) as key:
                    try:
                        val, vtype = winreg.QueryValueEx(key, value_name)
                        entry["data"] = val
                        entry["vtype"] = vtype
                    except FileNotFoundError:
                        entry["status"] = "нет значения"
                        entry["tag"] = "info"
                        missing += 1
                        self._restrict_cache.append(entry)
                        continue

                is_bad = False
                try:
                    if isinstance(val, int) and isinstance(bad_value, int):
                        is_bad = (int(val) == int(bad_value))
                    else:
                        is_bad = (str(val).strip().lower()
                                  == str(bad_value).strip().lower())
                except Exception:
                    is_bad = (val == bad_value)

                if is_bad:
                    entry["status"] = "ограничение"
                    entry["tag"] = "found"
                    found += 1
                else:
                    entry["status"] = "не критично"
                    entry["tag"] = "ok"
                    clean += 1
                self._restrict_cache.append(entry)
            except FileNotFoundError:
                missing += 1
            except PermissionError:
                entry["data"] = None
                entry["status"] = "нет доступа"
                entry["tag"] = "locked"
                self._restrict_cache.append(entry)
            except Exception as e:
                self.log(f"Ошибка чтения {subkey}\\{value_name}: {e}", "warn")

        self._render_restrictions()

        self.restrict_stats.config(
            text=(f"ограничение: {found}   ·   не критично: {clean}   ·   "
                  f"нет значения: {missing}   ·   всего: {len(catalog)}"))
        self.log(f"Сканирование: ограничение={found}, не критично={clean}.", "ok")
        self.toast("Ограничений не найдено" if found == 0 else f"Найдено: {found}",
                   "ok" if found == 0 else "warn")

    def _render_restrictions(self) -> None:
        if not hasattr(self, "restrict_tree"):
            return
        for row in self.restrict_tree.get_children():
            self.restrict_tree.delete(row)
        for it in self._restrict_cache:
            data = it.get("data")
            if data is None:
                data_str = "—"
            elif isinstance(data, int):
                data_str = f"0x{data:08X}" if data > 9 else str(data)
            else:
                data_str = str(data)
            self.restrict_tree.insert(
                "", "end",
                values=(it["category"], it["location"], it["value"],
                        data_str, it["status"]),
                tags=(it["tag"],),
            )

    def _get_selected_restriction(self) -> Optional[dict[str, Any]]:
        sel = self.restrict_tree.selection()
        if not sel:
            return None
        values = self.restrict_tree.item(sel[0], "values")
        loc, val = values[1], values[2]
        for it in self._restrict_cache:
            if it["location"] == loc and it["value"] == val:
                return it
        return None

    def _restrictions_backup_dir(self) -> str:
        d = os.path.join(self._backup_dir(), "restrictions")
        os.makedirs(d, exist_ok=True)
        return d

    def open_restrictions_backup_folder(self) -> None:
        self.open_path(self._restrictions_backup_dir())

    @staticmethod
    def _hive_name(hive: Any) -> str:
        if winreg is None:
            return "HKLM"
        return {winreg.HKEY_LOCAL_MACHINE: "HKLM",
                winreg.HKEY_CURRENT_USER: "HKCU",
                winreg.HKEY_CLASSES_ROOT: "HKCR",
                winreg.HKEY_USERS: "HKU",
                winreg.HKEY_CURRENT_CONFIG: "HKCC"}.get(hive, "HKLM")

    def _backup_restriction_key(self, item: dict[str, Any]) -> Optional[str]:
        """Экспортирует .reg-бэкап ветки. Возвращает путь или None."""
        hive_name = item["hive_name"]
        subkey = item["location"]
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        safe = re.sub(r"[\\/:*?\"<>|]+", "_", subkey)[-60:]
        out_path = os.path.join(self._restrictions_backup_dir(),
                                f"{hive_name}_{safe}_{ts}.reg")
        try:
            result = subprocess.run(
                ["reg", "export", f"{hive_name}\\{subkey}", out_path, "/y"],
                capture_output=True, timeout=20,
                creationflags=ConsoleRunner.CREATE_NO_WINDOW,
            )
            if result.returncode == 0 and os.path.exists(out_path):
                return out_path
            self.log(f"reg export rc={result.returncode} для {hive_name}\\{subkey}",
                     "warn")
        except Exception as e:
            self.log(f"reg export ошибка: {e}", "warn")
        return None

    def _write_restriction_value(self, item: dict[str, Any], new_value_str: str) -> bool:
        """Записывает новое значение в реестр. Возвращает True при успехе."""
        if winreg is None:
            return False
        vtype = item.get("vtype") or winreg.REG_DWORD
        try:
            if vtype == winreg.REG_DWORD:
                # принимаем и "0x1", и "1"
                s = new_value_str.strip()
                val_int = int(s, 16) if s.lower().startswith("0x") else int(s, 10)
                if val_int < 0 or val_int > 0xFFFFFFFF:
                    raise ValueError("значение вне диапазона DWORD")
                value = val_int
                rtype = winreg.REG_DWORD
            elif vtype == winreg.REG_SZ:
                value = new_value_str
                rtype = winreg.REG_SZ
            elif vtype == winreg.REG_EXPAND_SZ:
                value = new_value_str
                rtype = winreg.REG_EXPAND_SZ
            else:
                # по умолчанию — DWORD
                s = new_value_str.strip()
                val_int = int(s, 16) if s.lower().startswith("0x") else int(s, 10)
                value = val_int
                rtype = winreg.REG_DWORD

            with winreg.OpenKey(item["hive"], item["location"], 0,
                                winreg.KEY_SET_VALUE) as key:
                winreg.SetValueEx(key, item["value"], 0, rtype, value)
            return True
        except Exception as e:
            self.log(f"Ошибка записи {item['location']}\\{item['value']}: {e}", "err")
            return False

    def edit_selected_restriction(self) -> None:
        """Открывает диалог для ручного ввода нового значения."""
        item = self._get_selected_restriction()
        if not item:
            self.toast("Строка не выбрана", "warn")
            return
        self._open_edit_value_dialog(item)

    def _open_edit_value_dialog(self, item: dict[str, Any]) -> None:
        """Диалог ручного редактирования значения параметра."""
        win = tk.Toplevel(self.root)
        win.title("Изменение значения")
        win.geometry("560x360")
        win.configure(bg=PALETTE["bg_mid"])
        win.transient(self.root)
        win.grab_set()

        head = tk.Frame(win, bg=PALETTE["bg_card"], highlightthickness=1,
                        highlightbackground=PALETTE["border_hi"])
        head.pack(fill=tk.X, padx=16, pady=(16, 8))
        tk.Label(head, text=item["category"], bg=PALETTE["bg_card"],
                 fg=PALETTE["accent_hi"], font=FONT_H2
                 ).pack(anchor="w", padx=12, pady=(10, 2))
        tk.Label(head, text=f"{item['hive_name']}\\{item['location']}",
                 bg=PALETTE["bg_card"], fg=PALETTE["fg_dim"],
                 font=FONT_SMALL, wraplength=520, justify="left"
                 ).pack(anchor="w", padx=12)
        tk.Label(head, text=f"Параметр: {item['value']}",
                 bg=PALETTE["bg_card"], fg=PALETTE["fg"], font=FONT_UI_B
                 ).pack(anchor="w", padx=12, pady=(4, 10))

        form = tk.Frame(win, bg=PALETTE["bg_mid"])
        form.pack(fill=tk.X, padx=16, pady=(4, 6))

        cur = item.get("data")
        if cur is None:
            cur_str = ""
        elif isinstance(cur, int):
            cur_str = str(cur)
        else:
            cur_str = str(cur)

        tk.Label(form, text="Текущее значение:",
                 bg=PALETTE["bg_mid"], fg=PALETTE["fg_dim"], font=FONT_SMALL
                 ).pack(anchor="w")
        tk.Label(form, text=cur_str if cur_str else "—",
                 bg=PALETTE["bg_mid"], fg=PALETTE["fg"], font=FONT_MONO
                 ).pack(anchor="w", pady=(2, 10))

        tk.Label(form, text="Новое значение:",
                 bg=PALETTE["bg_mid"], fg=PALETTE["fg"], font=FONT_UI_B
                 ).pack(anchor="w")
        var = tk.StringVar(value=cur_str)
        entry = tk.Entry(form, textvariable=var, bg=PALETTE["bg_input"],
                         fg=PALETTE["fg"], insertbackground=PALETTE["accent"],
                         relief="flat", font=FONT_MONO)
        entry.pack(fill=tk.X, ipady=6, pady=(2, 4))
        entry.focus_set()
        entry.select_range(0, tk.END)

        hint = tk.Label(form,
                        text="Для DWORD — целое число (например 0, 1, 2) или hex (0x1).",
                        bg=PALETTE["bg_mid"], fg=PALETTE["fg_mute"], font=FONT_SMALL)
        hint.pack(anchor="w", pady=(0, 10))

        btns = tk.Frame(win, bg=PALETTE["bg_mid"])
        btns.pack(fill=tk.X, padx=16, pady=(0, 12))

        def apply_and_close() -> None:
            new_val = var.get().strip()
            if new_val == "":
                self.toast("Пустое значение", "warn")
                return
            backup = self._backup_restriction_key(item)
            if backup:
                self.log(f"Бэкап: {backup}", "ok")
            else:
                self.log("Не удалось создать бэкап — действие отменено.", "err")
                self.toast("Бэкап не создан, отмена", "err")
                return
            if self._write_restriction_value(item, new_val):
                self.log(f"Изменено: {item['location']}\\{item['value']} = {new_val}",
                         "ok")
                self.toast(f"Изменено: {item['value']} = {new_val}", "ok")
                win.destroy()
                self.scan_restrictions()
            else:
                self.toast("Ошибка записи", "err")

        def cancel() -> None:
            win.destroy()

        ttk.Button(btns, text="Применить", style="Success.TButton",
                   command=apply_and_close).pack(side=tk.RIGHT, padx=4)
        ttk.Button(btns, text="Отмена", style="Danger.TButton",
                   command=cancel).pack(side=tk.RIGHT, padx=4)
        ttk.Button(btns, text="Открыть ветку в regedit", style="Ghost.TButton",
                   command=lambda: self._open_regedit_at(item)
                   ).pack(side=tk.LEFT, padx=4)

        win.bind("<Return>", lambda e: apply_and_close())
        win.bind("<Escape>", lambda e: cancel())

    def open_selected_restriction_in_regedit(self) -> None:
        item = self._get_selected_restriction()
        if not item:
            self.toast("Строка не выбрана", "warn")
            return
        self._open_regedit_at(item)

    def _open_regedit_at(self, item: dict[str, Any]) -> None:
        """Открывает regedit и пытается перейти к ветке (насколько позволяет ОС)."""
        hive = item["hive_name"]
        sub = item["location"]
        try:
            # последний просмотренный ключ, чтобы regedit открылся на нём
            if winreg is not None:
                try:
                    with winreg.CreateKey(winreg.HKEY_CURRENT_USER,
                                          r"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit"):
                        with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
                                            r"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit",
                                            0, winreg.KEY_SET_VALUE) as k:
                            winreg.SetValueEx(k, "LastKey", 0, winreg.REG_SZ,
                                              f"Компьютер\\{hive}\\{sub}")
                except Exception:
                    pass
            subprocess.Popen(["regedit.exe"], shell=False)
            self.log(f"regedit: {hive}\\{sub}", "ok")
        except Exception as e:
            self.log(f"Не удалось открыть regedit: {e}", "err")

    def copy_selected_restriction_row(self) -> None:
        item = self._get_selected_restriction()
        if not item:
            return
        data = item.get("data")
        line = (f"{item['category']} | {item['hive_name']}\\{item['location']} "
                f"| {item['value']} = {data}")
        self.root.clipboard_clear()
        self.root.clipboard_append(line)
        self.toast("Строка скопирована", "ok")

    def export_restrictions_csv(self) -> None:
        if not self._restrict_cache:
            self.toast("Сначала выполните сканирование", "warn")
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV", "*.csv"), ("Все", "*.*")],
            initialfile=f"restrictions_{datetime.datetime.now():%Y%m%d_%H%M%S}.csv")
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8-sig", newline="") as f:
                w = csv.writer(f, delimiter=";")
                w.writerow(["Категория", "Расположение", "Параметр", "Значение", "Статус"])
                for it in self._restrict_cache:
                    w.writerow([it["category"], it["location"], it["value"],
                                it.get("data"), it["status"]])
            self.toast("CSV сохранён", "ok")
        except Exception as e:
            self.log(f"Ошибка экспорта: {e}", "err")

    def copy_restrictions_report(self) -> None:
        if not self._restrict_cache:
            self.toast("Сначала выполните сканирование", "warn")
            return
        lines = ["ZabHelper v4.3 — отчёт по ограничениям",
                 f"Время: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
                 "=" * 70]
        for it in self._restrict_cache:
            lines.append(f"[{it['status']}] {it['category']}")
            lines.append(f"  Расположение: {it['location']}")
            lines.append(f"  Параметр:     {it['value']} = {it.get('data')}")
            lines.append("")
        self.root.clipboard_clear()
        self.root.clipboard_append("\n".join(lines))
        self.toast("Отчёт скопирован в буфер", "ok")

    # ================================================================== #
    #  РАЗДЕЛ: ОТКРЫТЬ
    # ================================================================== #
    def _build_section_open(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Быстрый доступ", "")
        funcs: list[tuple[str, Callable[[], Any]]] = [
            ("Панель управления", lambda: self.open_exe("control")),
            ("Параметры Windows", lambda: self.open_exe("ms-settings:")),
            ("Диспетчер задач", lambda: self.open_exe("taskmgr.exe")),
            ("Проводник", lambda: self.open_exe("explorer.exe")),
            ("Редактор реестра", lambda: self.open_exe("regedit.exe")),
            ("Службы", lambda: self.open_exe("services.msc")),
            ("Планировщик", lambda: self.open_exe("taskschd.msc")),
            ("Принтеры", lambda: self.open_exe("control printers")),
            ("Диспетчер устройств", lambda: self.open_exe("devmgmt.msc")),
            ("Управление дисками", lambda: self.open_exe("diskmgmt.msc")),
            ("Программы", lambda: self.open_exe("appwiz.cpl")),
            ("Учётные записи", lambda: self.open_exe("netplwiz.exe")),
            ("Дата и время", lambda: self.open_exe("timedate.cpl")),
            ("Язык и регион", lambda: self.open_exe("intl.cpl")),
            ("Экран", lambda: self.open_exe("desk.cpl")),
            ("Звук", lambda: self.open_exe("mmsys.cpl")),
            ("Питание", lambda: self.open_exe("powercfg.cpl")),
            ("Сеть", lambda: self.open_exe("ncpa.cpl")),
            ("Брандмауэр", lambda: self.open_exe("wf.msc")),
            ("Defender", lambda: self.open_exe("windowsdefender:")),
            ("Windows Update", lambda: self.open_exe("ms-settings:windowsupdate")),
            ("Журнал событий", lambda: self.open_exe("eventvwr.msc")),
            ("Локальные пользователи", lambda: self.open_exe("lusrmgr.msc")),
            ("Компоненты Windows", lambda: self.open_exe("optionalfeatures.exe")),
            ("Монитор ресурсов", lambda: self.open_exe("resmon.exe")),
            ("Сведения о системе", lambda: self.open_exe("msinfo32.exe")),
            ("Очистка диска", lambda: self.open_exe("cleanmgr.exe")),
            ("Дефрагментация", lambda: self.open_exe("dfrgui.exe")),
            ("DirectX", lambda: self.open_exe("dxdiag.exe")),
            ("gpedit", lambda: self.open_exe("gpedit.msc")),
            ("msconfig", lambda: self.open_exe("msconfig.exe")),
            ("rstrui", lambda: self.open_exe("rstrui.exe")),
            ("Хранилище", lambda: self.open_exe("ms-settings:storagesense")),
            ("Общие папки", lambda: self.open_exe("fsmgmt.msc")),
            ("Таблица символов", lambda: self.open_exe("charmap.exe")),
        ]
        self._grid_buttons(body, funcs, cols=4)
        return frame

    # ================================================================== #
    #  РАЗДЕЛ: БЭКАПЫ
    # ================================================================== #
    def _build_section_backups(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        body = self._make_card(frame, "Бэкапы",
                               "Точки восстановления, экспорт реестра, hosts, автозагрузка.")
        funcs = [
            ("Точка восстановления", self.create_restore_point, "Accent.TButton"),
            ("rstrui", lambda: self.open_exe("rstrui.exe")),
            ("Экспорт HKCU\\Software", self.export_hkcu_software),
            ("Экспорт HKLM\\Software", self.export_hklm_software),
            ("Импорт .reg", self.import_reg_file),
            ("Бэкап hosts", self.backup_hosts),
            ("Бэкап автозагрузки", self.backup_startup),
            ("Бэкап программ", self.backup_installed),
            ("Открыть папку бэкапов", self.open_backup_folder),
        ]
        self._grid_buttons(body, funcs, cols=3)
        return frame

    # ================================================================== #
    #  РАЗДЕЛ: ДИАГНОСТИКА
    # ================================================================== #
    def _build_section_diagnostics(self, parent: tk.Frame) -> tk.Frame:
        frame = tk.Frame(parent, bg=PALETTE["bg_mid"])
        tb = tk.Frame(frame, bg=PALETTE["bg_mid"]); tb.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(tb, text="Собрать", style="Accent.TButton",
                   command=self.collect_diagnostics).pack(side=tk.LEFT)
        ttk.Button(tb, text="Сохранить", command=self.save_diagnostics
                   ).pack(side=tk.LEFT, padx=4)
        ttk.Button(tb, text="Копировать", command=self.copy_diagnostics
                   ).pack(side=tk.LEFT, padx=4)

        self.diag_text = scrolledtext.ScrolledText(
            frame, height=24, font=FONT_MONO,
            bg=PALETTE["bg_glass"], fg=PALETTE["fg"], relief="flat")
        self.diag_text.pack(fill=tk.BOTH, expand=True)
        self.progress = ttk.Progressbar(frame, mode="indeterminate")
        self.progress.pack(fill=tk.X, pady=(8, 0))
        return frame

    def collect_diagnostics(self) -> None:
        self.diag_text.delete("1.0", tk.END)
        def run() -> None:
            self.progress.start(10)
            try:
                self._append_diag("=== ZabHelper v4.3 · диагностический отчёт ===")
                self._append_diag(f"Время: {datetime.datetime.now()}")
                self._append_diag(f"Пользователь: {os.environ.get('USERNAME', '?')}")
                self._append_diag(f"Права администратора: {self.is_admin}")
                self._append_diag(f"Компьютер: {os.environ.get('COMPUTERNAME', '?')}")
                self._append_diag(f"Python: {sys.version}")
                self._append_diag("\n--- IP-конфигурация ---")
                self._append_diag(ConsoleRunner.run("ipconfig /all"))
                self._append_diag("\n--- Маршруты ---")
                self._append_diag(ConsoleRunner.run("route print"))
                self._append_diag("\n--- Defender ---")
                self._append_diag(ConsoleRunner.run_ps("Get-MpComputerStatus | Format-List"))
                self._append_diag("\n--- Диски ---")
                for drive in ("C:\\", "D:\\", "E:\\", "F:\\"):
                    if os.path.exists(drive):
                        try:
                            total, used, free = shutil.disk_usage(drive)
                            self._append_diag(f"{drive}  всего: {total/1e9:.1f} GB, "
                                              f"занято: {used/1e9:.1f} GB, "
                                              f"свободно: {free/1e9:.1f} GB")
                        except Exception:
                            pass
                self._append_diag("\n=== Отчёт завершён ===")
                self.log("Диагностика собрана.", "ok")
            finally:
                self.progress.stop()
        threading.Thread(target=run, daemon=True).start()

    def _append_diag(self, text: Any) -> None:
        try:
            self.diag_text.insert(tk.END, str(text) + "\n")
            self.diag_text.see(tk.END)
        except Exception:
            pass

    def save_diagnostics(self) -> None:
        path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Текстовые файлы", "*.txt"), ("Все", "*.*")],
            initialfile=f"zabhelper_diag_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt")
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.diag_text.get("1.0", tk.END))
            self.toast("Отчёт сохранён", "ok")
        except Exception as e:
            self.log(f"Ошибка сохранения: {e}", "err")

    def copy_diagnostics(self) -> None:
        self.root.clipboard_clear()
        self.root.clipboard_append(self.diag_text.get("1.0", tk.END))
        self.toast("Отчёт скопирован", "ok")

    # ================================================================== #
    #  ОБЩИЕ ФУНКЦИИ
    # ================================================================== #
    def reg_reset(self, subkey: str, value_name: str) -> None:
        if winreg is None:
            return
        if not self.is_admin:
            self.log(f"{value_name}: могут потребоваться права администратора.", "warn")
        success = False
        for hk, hk_name in [(winreg.HKEY_CURRENT_USER, "HKCU"),
                            (winreg.HKEY_LOCAL_MACHINE, "HKLM")]:
            try:
                key = winreg.OpenKey(hk, subkey, 0, winreg.KEY_SET_VALUE)
                winreg.SetValueEx(key, value_name, 0, winreg.REG_DWORD, 0)
                winreg.CloseKey(key)
                success = True
                self.log(f"Изменено '{value_name}' в {hk_name} -> 0.", "ok")
            except FileNotFoundError:
                continue
            except PermissionError:
                self.log(f"Нет прав на {value_name} в {hk_name}.", "err")
            except Exception as e:
                self.log(f"Ошибка {value_name}: {e}", "err")
        if not success:
            self.log(f"'{value_name}' не найден.", "info")
        else:
            self.toast(f"Изменено: {value_name} -> 0", "ok")

    def check_file(self, filename: str) -> None:
        path = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                            "System32", filename)
        if os.path.exists(path):
            self.log(f"{filename}: {os.path.getsize(path)} байт.", "ok")
        else:
            self.log(f"{filename} отсутствует!", "err")

    def restore_msconfig(self) -> None:
        self.reg_reset(r"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer",
                       "DisableMSConfig")
        self.reg_reset(
            r"Software\Policies\Microsoft\MMC\{8FC0B734-A0E1-11D1-A7D3-0000F87571E3}",
            "Restrict_Run")

    def restore_powershell(self) -> None:
        out = ConsoleRunner.run_ps("Get-ExecutionPolicy")
        self.log(f"Политика PowerShell: {out.strip()}", "info")
        self.run_quiet('powershell -NoProfile -Command "Set-ExecutionPolicy RemoteSigned '
                       '-Scope LocalMachine -Force"')
        self.log("Политика = RemoteSigned.", "ok")

    def restore_gpedit(self) -> None:
        self.reg_reset(r"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer",
                       "RestrictRun")

    def show_hidden_files(self) -> None:
        self._set_advanced("Hidden", 1, "Скрытые файлы включены.")

    def show_extensions(self) -> None:
        self._set_advanced("HideFileExt", 0, "Расширения включены.")

    def _set_advanced(self, name: str, value: int, ok_msg: str) -> None:
        if winreg is None:
            return
        try:
            key = winreg.OpenKey(
                winreg.HKEY_CURRENT_USER,
                r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
                0, winreg.KEY_SET_VALUE)
            winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value)
            winreg.CloseKey(key)
            self.log(ok_msg, "ok")
            self.toast(ok_msg, "ok")
        except Exception as e:
            self.log(f"Ошибка {name}: {e}", "err")

    def reset_explorer_view(self) -> None:
        if winreg is None:
            return
        try:
            key = winreg.OpenKey(
                winreg.HKEY_CURRENT_USER,
                r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
                0, winreg.KEY_SET_VALUE)
            for k, v in {"Hidden": 2, "HideFileExt": 1, "ShowSuperHidden": 0,
                         "ShowSyncProviderNotifications": 1, "LaunchTo": 1}.items():
                winreg.SetValueEx(key, k, 0, winreg.REG_DWORD, v)
            winreg.CloseKey(key)
            self.log("Настройки Проводника сброшены.", "ok")
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def reset_hosts(self) -> None:
        hosts = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                             r"System32\drivers\etc\hosts")
        if not self.ask_confirm("Сбросить hosts? Будет создан бэкап."):
            return
        try:
            if os.path.exists(hosts):
                shutil.copy2(hosts, hosts + f".bak_{datetime.datetime.now():%Y%m%d_%H%M%S}")
            with open(hosts, "w", encoding="utf-8") as f:
                f.write("# localhost name resolution is handled within DNS itself.\n"
                        "#\t127.0.0.1       localhost\n"
                        "#\t::1             localhost\n")
            self.log("hosts сброшен.", "ok")
        except Exception as e:
            self.log(f"Ошибка hosts: {e}", "err")

    def flush_dns(self) -> None:
        self.run_quiet("ipconfig /flushdns")
        self.log("DNS-кэш очищен.", "ok")
        self.toast("DNS-кэш очищен", "ok")

    def reset_winsock(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("netsh winsock reset")
        self.log("Winsock сброшен.", "ok")
        self.toast("Winsock сброшен", "ok")

    def reset_tcpip(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("netsh int ip reset")
        self.log("TCP/IP сброшен.", "ok")
        self.toast("TCP/IP сброшен", "ok")

    def reset_proxy(self) -> None:
        if winreg is None:
            return
        try:
            key = winreg.OpenKey(
                winreg.HKEY_CURRENT_USER,
                r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
                0, winreg.KEY_SET_VALUE)
            winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 0)
            try:
                winreg.DeleteValue(key, "ProxyServer")
            except FileNotFoundError:
                pass
            winreg.CloseKey(key)
            self.log("Прокси отключён.", "ok")
            self.toast("Прокси отключён", "ok")
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def restart_explorer(self) -> None:
        if not self.ask_confirm("Перезапустить проводник?"):
            return
        self.run_quiet("taskkill /f /im explorer.exe")
        subprocess.Popen("start explorer.exe", shell=True)
        self.log("Проводник перезапущен.", "ok")

    def unlock_wuauserv(self) -> None:
        self.set_service_start("wuauserv", "demand")
        self.set_service_start("UsoSvc", "auto")
        self.set_service_start("BITS", "delayed-auto")
        self.reg_reset(r"Software\Policies\Microsoft\Windows\WindowsUpdate\AU", "NoAutoUpdate")
        self.reg_reset(r"Software\Policies\Microsoft\Windows\WindowsUpdate",
                       "DisableWindowsUpdateAccess")

    def set_service_start(self, service: str, mode: str) -> None:
        if not self.is_admin:
            self.toast(f"{service}: нужны права администратора", "err"); return
        subprocess.run(f"sc config {service} start= {mode}",
                       shell=True, capture_output=True)
        subprocess.run(f"net start {service}",
                       shell=True, capture_output=True)
        self.log(f"Служба {service} -> {mode}", "ok")

    def restore_defender(self) -> None:
        if winreg is None:
            return
        try:
            key = winreg.OpenKey(
                winreg.HKEY_LOCAL_MACHINE,
                r"SOFTWARE\Policies\Microsoft\Windows Defender",
                0, winreg.KEY_SET_VALUE)
            for v in ("DisableAntiSpyware", "DisableAntiVirus"):
                try:
                    winreg.DeleteValue(key, v)
                    self.log(f"Удалено: {v}", "ok")
                except FileNotFoundError:
                    pass
            winreg.CloseKey(key)
            self.log("Политики Defender очищены.", "ok")
        except FileNotFoundError:
            self.log("Defender не заблокирован.", "info")
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def clear_ifeo(self) -> None:
        if winreg is None:
            return
        base = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
        try:
            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, base) as key:
                i, subs = 0, []
                while True:
                    try:
                        subs.append(winreg.EnumKey(key, i))
                        i += 1
                    except OSError:
                        break
            count = 0
            for sub in subs:
                try:
                    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
                                        f"{base}\\{sub}", 0, winreg.KEY_ALL_ACCESS) as sk:
                        try:
                            winreg.QueryValueEx(sk, "Debugger")
                            winreg.DeleteValue(sk, "Debugger")
                            count += 1
                            self.log(f"Удалён отладчик: {sub}", "ok")
                        except FileNotFoundError:
                            pass
                except Exception:
                    continue
            self.log(f"IFEO: удалено {count}.", "ok" if count else "info")
        except Exception as e:
            self.log(f"Ошибка IFEO: {e}", "err")

    def list_services(self) -> None:
        self.log("--- Службы ---", "info")
        out = ConsoleRunner.run("sc query type= service state= all")
        for line in out.splitlines()[:60]:
            if line.strip():
                self.log(line)

    def list_drivers(self) -> None:
        self.log("--- Драйверы ---", "info")
        out = ConsoleRunner.run("sc query type= driver state= all")
        for line in out.splitlines()[:50]:
            if line.strip():
                self.log(line)

    def reset_exe_assoc(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        subprocess.run(r'cmd /c assoc .exe=exefile && ftype exefile="%1" %*',
                       shell=True, capture_output=True)
        self.log("Ассоциации .exe восстановлены.", "ok")

    def disable_sticky_keys(self) -> None:
        if winreg is None:
            return
        try:
            key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
                                 r"Control Panel\Accessibility\StickyKeys",
                                 0, winreg.KEY_SET_VALUE)
            winreg.SetValueEx(key, "Flags", 0, winreg.REG_SZ, "506")
            winreg.CloseKey(key)
            self.log("Залипание клавиш отключено.", "ok")
        except Exception as e:
            self.log(f"Ошибка: {e}", "err")

    def clean_user_temp(self) -> None:
        temp = os.environ.get("TEMP", "")
        if not temp or not os.path.isdir(temp):
            self.toast("TEMP не найден", "warn"); return
        if not self.ask_confirm(f"Очистить Temp?\n{temp}"):
            return
        removed = failed = 0
        for entry in glob.glob(os.path.join(temp, "*")):
            try:
                if os.path.isfile(entry) or os.path.islink(entry):
                    os.remove(entry)
                else:
                    shutil.rmtree(entry, ignore_errors=True)
                removed += 1
            except Exception:
                failed += 1
        self.log(f"Temp: удалено {removed}, ошибок {failed}.", "ok")

    def clean_wu_cache(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        path = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                            "SoftwareDistribution", "Download")
        if not os.path.isdir(path):
            self.toast("Кэш не найден", "warn"); return
        if not self.ask_confirm(f"Очистить {path}?"):
            return
        self.run_quiet("net stop wuauserv")
        removed = 0
        for entry in glob.glob(os.path.join(path, "*")):
            try:
                if os.path.isdir(entry):
                    shutil.rmtree(entry, ignore_errors=True)
                else:
                    os.remove(entry)
                removed += 1
            except Exception:
                pass
        self.run_quiet("net start wuauserv")
        self.log(f"Кэш: удалено {removed}.", "ok")

    def clean_prefetch(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        path = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "Prefetch")
        removed = 0
        for entry in glob.glob(os.path.join(path, "*.pf")):
            try:
                os.remove(entry); removed += 1
            except Exception:
                pass
        self.log(f"Prefetch: удалено {removed}.", "ok")

    def clean_recycle_bin(self) -> None:
        if not self.ask_confirm("Очистить корзину?"):
            return
        ConsoleRunner.run_ps("Clear-RecycleBin -Force", timeout=60)
        self.log("Корзина очищена.", "ok")

    def open_cmd_desktop(self) -> None:
        desktop = os.path.join(os.path.expanduser("~"), "Desktop")
        subprocess.Popen(f'start cmd.exe /k "cd /d {desktop}"', shell=True)
        self.log("CMD открыт.", "ok")

    def confirm_reboot(self) -> None:
        if self.ask_confirm("Перезагрузить через 60 секунд?"):
            self.run_quiet("shutdown /r /t 60")
            self.log("Перезагрузка запланирована.", "warn")

    def run_cmd_async(self, command: str) -> None:
        if not self.is_admin:
            self.toast(f"'{command}' требует админа", "err"); return
        subprocess.Popen(f'start cmd.exe /k "chcp 65001 >nul && {command}"', shell=True)
        self.log(f"Запущено: {command}", "ok")

    def run_quiet(self, command: str) -> None:
        try:
            subprocess.run(f'chcp 65001 >nul && {command}',
                           shell=True, capture_output=True, timeout=180)
        except Exception as e:
            self.log(f"Ошибка '{command}': {e}", "err")

    def open_exe(self, exe: str) -> None:
        try:
            subprocess.Popen(exe, shell=True)
            self.log(f"Запущено: {exe}", "ok")
        except Exception as e:
            self.log(f"Не удалось: {exe}: {e}", "err")

    def open_path(self, path: str) -> None:
        try:
            os.startfile(path)
            self.log(f"Открыто: {path}", "ok")
        except Exception as e:
            self.log(f"Не удалось открыть {path}: {e}", "err")

    # ---- бэкапы ---- #
    def _backup_dir(self) -> str:
        d = os.path.join(os.path.expanduser("~"), "ZabHelper_Backups")
        os.makedirs(d, exist_ok=True)
        return d

    def open_backup_folder(self) -> None:
        self.open_path(self._backup_dir())

    def create_restore_point(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        name = f"ZabHelper_{datetime.datetime.now():%Y%m%d_%H%M}"
        subprocess.run(
            f'powershell -NoProfile -Command "'
            f'Checkpoint-Computer -Description \'{name}\' -RestorePointType MODIFY_SETTINGS"',
            shell=True, capture_output=True, timeout=300)
        self.log(f"Точка восстановления: {name}", "ok")

    def export_hkcu_software(self) -> None:
        path = os.path.join(self._backup_dir(),
                            f"HKCU_Software_{datetime.datetime.now():%Y%m%d_%H%M%S}.reg")
        subprocess.run(f'reg export HKCU\\Software "{path}" /y',
                       shell=True, capture_output=True)
        self.log(f"Экспорт: {path}", "ok")

    def export_hklm_software(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        path = os.path.join(self._backup_dir(),
                            f"HKLM_Software_{datetime.datetime.now():%Y%m%d_%H%M%S}.reg")
        subprocess.run(f'reg export HKLM\\Software "{path}" /y',
                       shell=True, capture_output=True)
        self.log(f"Экспорт: {path}", "ok")

    def import_reg_file(self) -> None:
        path = filedialog.askopenfilename(filetypes=[("Reg", "*.reg"), ("Все", "*.*")])
        if not path:
            return
        if not self.ask_confirm(f"Импортировать реестр?\n{path}"):
            return
        subprocess.run(f'reg import "{path}"', shell=True, capture_output=True)
        self.log(f"Импорт: {path}", "ok")

    def backup_hosts(self) -> None:
        hosts = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                             r"System32\drivers\etc\hosts")
        if not os.path.exists(hosts):
            self.toast("hosts не найден", "warn"); return
        dst = os.path.join(self._backup_dir(),
                           f"hosts_{datetime.datetime.now():%Y%m%d_%H%M%S}.bak")
        shutil.copy2(hosts, dst)
        self.log(f"Бэкап hosts: {dst}", "ok")

    def backup_startup(self) -> None:
        self.refresh_startup()
        dst = os.path.join(self._backup_dir(),
                           f"startup_{datetime.datetime.now():%Y%m%d_%H%M%S}.json")
        with open(dst, "w", encoding="utf-8") as f:
            json.dump(self._startup_cache, f, ensure_ascii=False, indent=2, default=str)
        self.log(f"Бэкап автозагрузки: {dst}", "ok")

    def backup_installed(self) -> None:
        out = ConsoleRunner.run_ps(
            "Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* "
            "| Select-Object DisplayName,DisplayVersion,Publisher | Format-Table -AutoSize")
        dst = os.path.join(self._backup_dir(),
                           f"installed_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt")
        with open(dst, "w", encoding="utf-8") as f:
            f.write(out)
        self.log(f"Программы сохранены: {dst}", "ok")

    # ---- безопасность ---- #
    def check_defender_status(self) -> None:
        out = ConsoleRunner.run_ps("Get-MpComputerStatus | Format-List")
        self.log("--- Defender ---", "info")
        for line in out.splitlines():
            if line.strip():
                self.log(line)

    def check_firewall_status(self) -> None:
        out = ConsoleRunner.run("netsh advfirewall show allprofiles state")
        self.log("--- Брандмауэр ---", "info")
        for line in out.splitlines():
            if line.strip():
                self.log(line)

    def check_uac_status(self) -> None:
        if winreg is None:
            return
        try:
            with winreg.OpenKey(
                winreg.HKEY_LOCAL_MACHINE,
                r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
            ) as key:
                val, _ = winreg.QueryValueEx(key, "EnableLUA")
                level, _ = winreg.QueryValueEx(key, "ConsentPromptBehaviorAdmin")
            self.log(f"UAC EnableLUA = {val}", "ok" if val == 1 else "warn")
            self.log(f"Уровень: {level}", "info")
        except Exception as e:
            self.log(f"Ошибка UAC: {e}", "err")

    def check_defender_threats(self) -> None:
        out = ConsoleRunner.run_ps(
            "Get-MpThreatDetection | Select-Object -First 10 "
            "InitialDetectionTime,ThreatID,ActionSuccess | Format-Table -AutoSize")
        self.log("--- Угрозы ---", "info")
        for line in out.splitlines():
            if line.strip():
                self.log(line)

    def run_defender_quick(self) -> None:
        self.run_cmd_async('powershell -NoProfile -Command "Start-MpScan -ScanType QuickScan"')

    def run_defender_full(self) -> None:
        if self.ask_confirm("Полная проверка Defender может занять часы. Продолжить?"):
            self.run_cmd_async('powershell -NoProfile -Command "Start-MpScan -ScanType FullScan"')

    def update_defender(self) -> None:
        self.run_cmd_async('powershell -NoProfile -Command "Update-MpSignature"')

    def enable_firewall(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("netsh advfirewall set allprofiles state on")
        self.log("Брандмауэр включён.", "ok")

    def enable_uac(self) -> None:
        if winreg is None:
            return
        try:
            with winreg.OpenKey(
                winreg.HKEY_LOCAL_MACHINE,
                r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",
                0, winreg.KEY_SET_VALUE
            ) as key:
                winreg.SetValueEx(key, "EnableLUA", 0, winreg.REG_DWORD, 1)
                winreg.SetValueEx(key, "ConsentPromptBehaviorAdmin", 0, winreg.REG_DWORD, 5)
            self.log("UAC включён (перезагрузка).", "ok")
        except Exception as e:
            self.log(f"Ошибка UAC: {e}", "err")

    def list_admins(self) -> None:
        out = ConsoleRunner.run('net localgroup Administrators')
        self.log("--- Администраторы ---", "info")
        for line in out.splitlines():
            if line.strip():
                self.log(line)

    # ---- восстановление ---- #
    def repair_network(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        for c in ("netsh winsock reset", "netsh int ip reset",
                  "ipconfig /flushdns", "ipconfig /release", "ipconfig /renew"):
            self.run_quiet(c)
        self.log("Сетевой стек сброшен.", "ok")

    def repair_windows_update(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        for svc in ("wuauserv", "cryptSvc", "bits", "msiserver"):
            self.run_quiet(f"net stop {svc}")
        for folder in ("SoftwareDistribution", "catroot2"):
            p = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), folder)
            if os.path.isdir(p):
                shutil.rmtree(p, ignore_errors=True)
                self.log(f"Удалено: {p}", "ok")
        for svc in ("wuauserv", "cryptSvc", "bits", "msiserver"):
            self.run_quiet(f"net start {svc}")
        self.log("Windows Update сброшен.", "ok")

    def repair_store(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_cmd_async("wsreset.exe")

    def repair_fonts(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("net stop FontCache")
        cache = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
                             "ServiceProfiles", "LocalService", "AppData",
                             "Local", "FontCache")
        if os.path.isdir(cache):
            for f in glob.glob(os.path.join(cache, "*.dat")):
                try:
                    os.remove(f)
                except Exception:
                    pass
        self.run_quiet("net start FontCache")
        self.log("Кэш шрифтов пересобран.", "ok")

    def repair_scheduler(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("net stop Schedule")
        self.run_quiet("net start Schedule")
        self.log("Планировщик перезапущен.", "ok")

    def repair_wmi(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("net stop winmgmt /y")
        self.run_quiet("winmgmt /resetrepository")
        self.run_quiet("net start winmgmt")
        self.log("WMI пересобран.", "ok")

    def repair_firewall(self) -> None:
        if not self.is_admin:
            self.toast("Нужны права администратора", "err"); return
        self.run_quiet("netsh advfirewall reset")
        self.run_quiet("netsh advfirewall set allprofiles state on")
        self.log("Брандмауэр сброшен.", "ok")

    # ---- горячие клавиши ---- #
    def _bind_hotkeys(self) -> None:
        self.root.bind("<F5>", lambda e: self._refresh_current_section())
        self.root.bind("<Control-s>", lambda e: self.save_log())
        self.root.bind("<Control-l>", lambda e: self.clear_log())
        self.root.bind("<Control-q>", lambda e: self.root.destroy())

    def _refresh_current_section(self) -> None:
        key = self._current_section
        if key == "monitor":
            self.refresh_processes()
        elif key == "running":
            self.refresh_running()
        elif key == "startup":
            self.refresh_startup()
        elif key == "restrict":
            self.scan_restrictions()
        self.toast("Обновлено (F5)", "info")


# ====================================================================== #
#  ТОЧКА ВХОДА
# ====================================================================== #
def main() -> int:
    if sys.platform != "win32":
        print("ZabHelper рассчитан на Windows.", file=sys.stderr)
        return 1
    root = tk.Tk()
    app = ZabHelperApp(root)
    root.update_idletasks()
    w, h = root.winfo_width(), root.winfo_height()
    x = (root.winfo_screenwidth() - w) // 2
    y = (root.winfo_screenheight() - h) // 2
    root.geometry(f"+{x}+{y}")
    try:
        root.mainloop()
    finally:
        app.log("ZabHelper завершён.", "info")
    return 0


if __name__ == "__main__":
    sys.exit(main())